* Dep bump * Fetch transactions when safe view is mounted * eslint fix * Calculate new tx nonce from latest tx in service * Fix tx cancellation, allow passing nonce to createTransaction * dep bump * Refactor createTransaction/processTransaction to use object as argument * Adopting transactions table to new send tx flow with predicted nonces * dep bump, disable esModule in file-loader options after new v5 release * Don't show older tx annotation for already executed txs * sort tx by nonce * get new safe nonce after tx execution * Bugfixes * remove whitespace for showOlderTxAnnotation
This commit is contained in:
parent
87a7796a84
commit
bc7d5836f6
|
@ -3,7 +3,6 @@
|
|||
<PROJECT_ROOT>/contracts/**/.*
|
||||
<PROJECT_ROOT>/scripts/**/.*
|
||||
<PROJECT_ROOT>/public/**/.*
|
||||
<PROJECT_ROOT>/src/test/**/.*
|
||||
<PROJECT_ROOT>/babel.config.js
|
||||
<PROJECT_ROOT>/jest.config.js
|
||||
<PROJECT_ROOT>/truffle.js
|
||||
|
|
|
@ -13,7 +13,7 @@ type Props = {
|
|||
margin?: Size,
|
||||
padding?: Size,
|
||||
justify?: 'center' | 'right' | 'left' | 'space-around',
|
||||
children: React.Node,
|
||||
children?: React.Node,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
padding: 80px 200px 0px 200px;
|
||||
padding: 135px 200px 0px 200px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $(screenLg)px) {
|
||||
.page {
|
||||
padding: 80px $lg 0px $lg;
|
||||
padding: 135px $lg 0px $lg;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,15 +79,15 @@ const ReviewCustomTx = ({
|
|||
const txData = tx.data.trim()
|
||||
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : 0
|
||||
|
||||
createTransaction(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txValue,
|
||||
to: txRecipient,
|
||||
valueInWei: txValue,
|
||||
txData,
|
||||
TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
|
|
@ -109,15 +109,15 @@ const ReviewTx = ({
|
|||
txAmount = 0
|
||||
}
|
||||
|
||||
createTransaction(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txAmount,
|
||||
to: txRecipient,
|
||||
valueInWei: txAmount,
|
||||
txData,
|
||||
TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
|
|
@ -176,6 +176,7 @@ const Layout = (props: Props) => {
|
|||
<Transactions
|
||||
threshold={safe.threshold}
|
||||
owners={safe.owners}
|
||||
nonce={safe.nonce}
|
||||
transactions={transactions}
|
||||
fetchTransactions={fetchTransactions}
|
||||
safeAddress={address}
|
||||
|
|
|
@ -47,15 +47,15 @@ export const sendAddOwner = async (
|
|||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = gnosisSafe.contract.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||
|
||||
const txHash = await createTransaction(
|
||||
const txHash = await createTransaction({
|
||||
safeAddress,
|
||||
safeAddress,
|
||||
0,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
txData,
|
||||
TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
})
|
||||
|
||||
if (txHash) {
|
||||
addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress })
|
||||
|
|
|
@ -62,15 +62,15 @@ export const sendRemoveOwner = async (
|
|||
.removeOwner(prevAddress, ownerAddressToRemove, values.threshold)
|
||||
.encodeABI()
|
||||
|
||||
const txHash = await createTransaction(
|
||||
const txHash = await createTransaction({
|
||||
safeAddress,
|
||||
safeAddress,
|
||||
0,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
txData,
|
||||
TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
})
|
||||
|
||||
if (txHash && safe.threshold === 1) {
|
||||
removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })
|
||||
|
|
|
@ -58,15 +58,15 @@ export const sendReplaceOwner = async (
|
|||
.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress)
|
||||
.encodeABI()
|
||||
|
||||
const txHash = await createTransaction(
|
||||
const txHash = await createTransaction({
|
||||
safeAddress,
|
||||
safeAddress,
|
||||
0,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
txData,
|
||||
TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
})
|
||||
|
||||
if (txHash && safe.threshold === 1) {
|
||||
replaceSafeOwner({
|
||||
|
|
|
@ -47,15 +47,15 @@ const ThresholdSettings = ({
|
|||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = safeInstance.contract.methods.changeThreshold(newThreshold).encodeABI()
|
||||
|
||||
createTransaction(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
safeAddress,
|
||||
0,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
txData,
|
||||
TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -33,8 +33,9 @@ type Props = {
|
|||
threshold: number,
|
||||
thresholdReached: boolean,
|
||||
userAddress: string,
|
||||
canExecute: boolean,
|
||||
enqueueSnackbar: Function,
|
||||
closeSnackbar: Function,
|
||||
closeSnackbar: Function
|
||||
}
|
||||
|
||||
const getModalTitleAndDescription = (thresholdReached: boolean) => {
|
||||
|
@ -59,15 +60,16 @@ const ApproveTxModal = ({
|
|||
tx,
|
||||
safeAddress,
|
||||
threshold,
|
||||
canExecute,
|
||||
thresholdReached,
|
||||
userAddress,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
}: Props) => {
|
||||
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
||||
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(oneConfirmationLeft || thresholdReached)
|
||||
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(canExecute)
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
const { title, description } = getModalTitleAndDescription(thresholdReached)
|
||||
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
@ -100,20 +102,25 @@ const ApproveTxModal = ({
|
|||
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
||||
|
||||
const approveTx = () => {
|
||||
processTransaction(
|
||||
processTransaction({
|
||||
safeAddress,
|
||||
tx,
|
||||
userAddress,
|
||||
TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
approveAndExecute && oneConfirmationLeft,
|
||||
)
|
||||
approveAndExecute: canExecute && approveAndExecute && oneConfirmationLeft,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={title} description={description} handleClose={onClose} open={isOpen}>
|
||||
<Modal
|
||||
title={title}
|
||||
description={description}
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||
{title}
|
||||
|
@ -131,14 +138,21 @@ const ApproveTxModal = ({
|
|||
<br />
|
||||
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||
</Paragraph>
|
||||
{oneConfirmationLeft && (
|
||||
{oneConfirmationLeft && canExecute && (
|
||||
<>
|
||||
<Paragraph color="error">
|
||||
Approving this transaction executes it right away. If you want approve but execute the transaction
|
||||
manually later, click on the checkbox below.
|
||||
Approving this transaction executes it right away. If you want
|
||||
approve but execute the transaction manually later, click on the
|
||||
checkbox below.
|
||||
</Paragraph>
|
||||
<FormControlLabel
|
||||
control={<Checkbox onChange={handleExecuteCheckbox} checked={approveAndExecute} color="primary" />}
|
||||
control={(
|
||||
<Checkbox
|
||||
onChange={handleExecuteCheckbox}
|
||||
checked={approveAndExecute}
|
||||
color="primary"
|
||||
/>
|
||||
)}
|
||||
label="Execute transaction"
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -64,15 +64,15 @@ const CancelTxModal = ({
|
|||
}, [])
|
||||
|
||||
const sendReplacementTransaction = () => {
|
||||
createTransaction(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
safeAddress,
|
||||
0,
|
||||
EMPTY_DATA,
|
||||
TX_NOTIFICATION_TYPES.CANCELLATION_TX,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
txNonce: tx.nonce,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ const ButtonRow = ({
|
|||
onTxCancel,
|
||||
showCancelBtn,
|
||||
}: Props) => (
|
||||
<Row align="right" className={classes.buttonRow}>
|
||||
<Row align="end" className={classes.buttonRow}>
|
||||
{showCancelBtn && (
|
||||
<Button className={classes.button} variant="contained" minWidth={140} color="secondary" onClick={onTxCancel}>
|
||||
Cancel tx
|
||||
|
|
|
@ -26,7 +26,7 @@ type OwnerProps = {
|
|||
showConfirmBtn: boolean,
|
||||
showExecuteBtn: boolean,
|
||||
onTxConfirm: Function,
|
||||
onTxExecute: Function,
|
||||
onTxExecute: Function
|
||||
}
|
||||
|
||||
const OwnerComponent = ({
|
||||
|
@ -42,21 +42,33 @@ const OwnerComponent = ({
|
|||
thresholdReached,
|
||||
}: OwnerProps) => (
|
||||
<Block className={classes.container}>
|
||||
<div className={confirmed || thresholdReached || executor
|
||||
? classes.verticalLineProgressDone
|
||||
: classes.verticalLineProgressPending}
|
||||
<div
|
||||
className={
|
||||
confirmed || thresholdReached || executor
|
||||
? classes.verticalLineProgressDone
|
||||
: classes.verticalLineProgressPending
|
||||
}
|
||||
/>
|
||||
<div className={classes.iconState}>
|
||||
{confirmed
|
||||
? <Img src={ConfirmSmallFilledIcon} />
|
||||
: thresholdReached || executor ? <Img src={ConfirmSmallGreenIcon} /> : <Img src={ConfirmSmallGreyIcon} />}
|
||||
{confirmed ? (
|
||||
<Img src={ConfirmSmallFilledIcon} />
|
||||
) : thresholdReached || executor ? (
|
||||
<Img src={ConfirmSmallGreenIcon} />
|
||||
) : (
|
||||
<Img src={ConfirmSmallGreyIcon} />
|
||||
)}
|
||||
</div>
|
||||
<Identicon address={owner.address} diameter={32} className={classes.icon} />
|
||||
<Block>
|
||||
<Paragraph className={classes.name} noMargin>
|
||||
{owner.name}
|
||||
</Paragraph>
|
||||
<EtherscanLink className={classes.address} type="address" value={owner.address} cut={4} />
|
||||
<EtherscanLink
|
||||
className={classes.address}
|
||||
type="address"
|
||||
value={owner.address}
|
||||
cut={4}
|
||||
/>
|
||||
</Block>
|
||||
<Block className={classes.spacer} />
|
||||
{showConfirmBtn && owner.address === userAddress && (
|
||||
|
|
|
@ -16,6 +16,7 @@ import CheckLargeFilledGreenIcon from './assets/check-large-filled-green.svg'
|
|||
import ConfirmLargeGreenIcon from './assets/confirm-large-green.svg'
|
||||
import ConfirmLargeGreyIcon from './assets/confirm-large-grey.svg'
|
||||
import { styles } from './style'
|
||||
import Paragraph from '~/components/layout/Paragraph/index'
|
||||
|
||||
type Props = {
|
||||
tx: Transaction,
|
||||
|
@ -26,15 +27,13 @@ type Props = {
|
|||
userAddress: string,
|
||||
thresholdReached: boolean,
|
||||
safeAddress: string,
|
||||
canExecute: boolean,
|
||||
onTxConfirm: Function,
|
||||
onTxCancel: Function,
|
||||
onTxExecute: Function,
|
||||
onTxExecute: Function
|
||||
}
|
||||
|
||||
const isCancellationTransaction = (
|
||||
tx: Transaction,
|
||||
safeAddress: string,
|
||||
) => !tx.value && tx.data === EMPTY_DATA && tx.recipient === safeAddress
|
||||
const isCancellationTransaction = (tx: Transaction, safeAddress: string) => !tx.value && tx.data === EMPTY_DATA && tx.recipient === safeAddress
|
||||
|
||||
const OwnersColumn = ({
|
||||
tx,
|
||||
|
@ -48,8 +47,10 @@ const OwnersColumn = ({
|
|||
onTxConfirm,
|
||||
onTxCancel,
|
||||
onTxExecute,
|
||||
canExecute,
|
||||
}: Props) => {
|
||||
const cancellationTx = isCancellationTransaction(tx, safeAddress)
|
||||
const showOlderTxAnnotation = thresholdReached && !canExecute && !tx.isExecuted
|
||||
|
||||
const ownersWhoConfirmed = []
|
||||
let currentUserAlreadyConfirmed = false
|
||||
|
@ -62,7 +63,9 @@ const OwnersColumn = ({
|
|||
}
|
||||
})
|
||||
const ownersUnconfirmed = owners.filter(
|
||||
(owner) => tx.confirmations.findIndex((conf) => conf.owner.address === owner.address) === -1,
|
||||
(owner) => tx.confirmations.findIndex(
|
||||
(conf) => conf.owner.address === owner.address,
|
||||
) === -1,
|
||||
)
|
||||
let userIsUnconfirmedOwner
|
||||
ownersUnconfirmed.some((owner) => {
|
||||
|
@ -77,25 +80,39 @@ const OwnersColumn = ({
|
|||
} else if (tx.status === 'cancelled') {
|
||||
// tx is cancelled (replaced) by another one
|
||||
displayButtonRow = false
|
||||
} else if (cancellationTx && currentUserAlreadyConfirmed && !thresholdReached) {
|
||||
} else if (
|
||||
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
|
||||
}
|
||||
|
||||
const showConfirmBtn = !tx.isExecuted
|
||||
&& tx.status !== 'pending'
|
||||
&& !tx.cancelled
|
||||
&& userIsUnconfirmedOwner
|
||||
&& !currentUserAlreadyConfirmed
|
||||
&& !thresholdReached
|
||||
|
||||
const showExecuteBtn = canExecute && !tx.isExecuted && thresholdReached
|
||||
|
||||
return (
|
||||
<Col xs={6} className={classes.rightCol} layout="block">
|
||||
<Block className={cn(classes.ownerListTitle, (thresholdReached || tx.isExecuted) && classes.ownerListTitleDone)}>
|
||||
<Block
|
||||
className={cn(
|
||||
classes.ownerListTitle,
|
||||
(thresholdReached || tx.isExecuted) && classes.ownerListTitleDone,
|
||||
)}
|
||||
>
|
||||
<div className={classes.iconState}>
|
||||
{thresholdReached || tx.isExecuted
|
||||
? <Img src={CheckLargeFilledGreenIcon} />
|
||||
: <Img src={ConfirmLargeGreenIcon} />}
|
||||
{thresholdReached || tx.isExecuted ? (
|
||||
<Img src={CheckLargeFilledGreenIcon} />
|
||||
) : (
|
||||
<Img src={ConfirmLargeGreenIcon} />
|
||||
)}
|
||||
</div>
|
||||
{tx.isExecuted
|
||||
? `Confirmed [${tx.confirmations.size}/${tx.confirmations.size}]`
|
||||
|
@ -110,25 +127,43 @@ const OwnersColumn = ({
|
|||
onTxConfirm={onTxConfirm}
|
||||
onTxExecute={onTxExecute}
|
||||
showConfirmBtn={showConfirmBtn}
|
||||
showExecuteBtn={!tx.isExecuted && thresholdReached}
|
||||
showExecuteBtn={showExecuteBtn}
|
||||
/>
|
||||
<Block className={cn(classes.ownerListTitle, tx.isExecuted && classes.ownerListTitleDone)}>
|
||||
<div className={thresholdReached || tx.isExecuted
|
||||
? classes.verticalLineProgressDone
|
||||
: classes.verticalLineProgressPending}
|
||||
<Block
|
||||
className={cn(
|
||||
classes.ownerListTitle,
|
||||
tx.isExecuted && classes.ownerListTitleDone,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
thresholdReached || tx.isExecuted
|
||||
? classes.verticalLineProgressDone
|
||||
: classes.verticalLineProgressPending
|
||||
}
|
||||
/>
|
||||
<div className={classes.iconState}>
|
||||
{!thresholdReached && !tx.isExecuted && <Img src={ConfirmLargeGreyIcon} />}
|
||||
{thresholdReached && !tx.isExecuted && <Img src={ConfirmLargeGreenIcon} />}
|
||||
{tx.isExecuted && <Img src={CheckLargeFilledGreenIcon} />}
|
||||
{!thresholdReached && !tx.isExecuted && (
|
||||
<Img src={ConfirmLargeGreyIcon} alt="Confirm tx" />
|
||||
)}
|
||||
{thresholdReached && !tx.isExecuted && (
|
||||
<Img src={ConfirmLargeGreenIcon} alt="Execute tx" />
|
||||
)}
|
||||
{tx.isExecuted && (
|
||||
<Img src={CheckLargeFilledGreenIcon} alt="TX Executed icon" />
|
||||
)}
|
||||
</div>
|
||||
Executed
|
||||
</Block>
|
||||
{showOlderTxAnnotation && (
|
||||
<Block className={classes.olderTxAnnotation}>
|
||||
<Paragraph>
|
||||
There are older transactions that need to be executed first
|
||||
</Paragraph>
|
||||
</Block>
|
||||
)}
|
||||
{granted && displayButtonRow && (
|
||||
<ButtonRow
|
||||
onTxCancel={onTxCancel}
|
||||
showCancelBtn={!cancellationTx}
|
||||
/>
|
||||
<ButtonRow onTxCancel={onTxCancel} showCancelBtn={!cancellationTx} />
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
|
|
|
@ -53,6 +53,9 @@ export const styles = () => ({
|
|||
textTransform: 'uppercase',
|
||||
letterSpacing: '1px',
|
||||
},
|
||||
olderTxAnnotation: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
ownerListTitleDone: {
|
||||
color: secondary,
|
||||
},
|
||||
|
|
|
@ -29,6 +29,7 @@ type Props = {
|
|||
safeAddress: string,
|
||||
createTransaction: Function,
|
||||
processTransaction: Function,
|
||||
nonce: number,
|
||||
}
|
||||
|
||||
type OpenModal = 'cancelTx' | 'approveTx' | null
|
||||
|
@ -52,12 +53,14 @@ const ExpandedTx = ({
|
|||
safeAddress,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
nonce,
|
||||
}: Props) => {
|
||||
const [openModal, setOpenModal] = useState<OpenModal>(null)
|
||||
const openApproveModal = () => setOpenModal('approveTx')
|
||||
const openCancelModal = () => setOpenModal('cancelTx')
|
||||
const closeModal = () => setOpenModal(null)
|
||||
const thresholdReached = threshold <= tx.confirmations.size
|
||||
const canExecute = nonce === tx.nonce
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -117,6 +120,7 @@ const ExpandedTx = ({
|
|||
tx={tx}
|
||||
owners={owners}
|
||||
granted={granted}
|
||||
canExecute={canExecute}
|
||||
threshold={threshold}
|
||||
userAddress={userAddress}
|
||||
thresholdReached={thresholdReached}
|
||||
|
@ -141,6 +145,7 @@ const ExpandedTx = ({
|
|||
isOpen
|
||||
processTransaction={processTransaction}
|
||||
onClose={closeModal}
|
||||
canExecute={canExecute}
|
||||
tx={tx}
|
||||
userAddress={userAddress}
|
||||
safeAddress={safeAddress}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { type Transaction } from '~/routes/safe/store/models/transaction'
|
|||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import ExpandedTxComponent from './ExpandedTx'
|
||||
import {
|
||||
getTxTableData, generateColumns, TX_TABLE_DATE_ID, type TransactionRow, TX_TABLE_RAW_TX_ID,
|
||||
getTxTableData, generateColumns, TX_TABLE_NONCE_ID, type TransactionRow, TX_TABLE_RAW_TX_ID,
|
||||
} from './columns'
|
||||
import { styles } from './style'
|
||||
import Status from './Status'
|
||||
|
@ -37,6 +37,7 @@ type Props = {
|
|||
userAddress: string,
|
||||
granted: boolean,
|
||||
safeAddress: string,
|
||||
nonce: number,
|
||||
createTransaction: Function,
|
||||
processTransaction: Function,
|
||||
}
|
||||
|
@ -51,6 +52,7 @@ const TxsTable = ({
|
|||
safeAddress,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
nonce,
|
||||
}: Props) => {
|
||||
const [expandedTx, setExpandedTx] = useState<string | null>(null)
|
||||
|
||||
|
@ -66,7 +68,7 @@ const TxsTable = ({
|
|||
<Block className={classes.container}>
|
||||
<Table
|
||||
label="Transactions"
|
||||
defaultOrderBy={TX_TABLE_DATE_ID}
|
||||
defaultOrderBy={TX_TABLE_NONCE_ID}
|
||||
defaultOrder="desc"
|
||||
defaultRowsPerPage={25}
|
||||
columns={columns}
|
||||
|
@ -126,6 +128,7 @@ const TxsTable = ({
|
|||
createTransaction={createTransaction}
|
||||
processTransaction={processTransaction}
|
||||
safeAddress={safeAddress}
|
||||
nonce={nonce}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { List } from 'immutable'
|
||||
import TxsTable from '~/routes/safe/components/Transactions/TxsTable'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
|
@ -14,9 +14,13 @@ type Props = {
|
|||
granted: boolean,
|
||||
createTransaction: Function,
|
||||
processTransaction: Function,
|
||||
fetchTransactions: Function,
|
||||
currentNetwork: string,
|
||||
nonce: number,
|
||||
}
|
||||
|
||||
const TIMEOUT = 5000
|
||||
|
||||
const Transactions = ({
|
||||
transactions = List(),
|
||||
owners,
|
||||
|
@ -26,8 +30,23 @@ const Transactions = ({
|
|||
safeAddress,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTransactions,
|
||||
currentNetwork,
|
||||
}: Props) => (
|
||||
nonce,
|
||||
}: Props) => {
|
||||
let intervalId: IntervalID
|
||||
|
||||
useEffect(() => {
|
||||
fetchTransactions(safeAddress)
|
||||
|
||||
intervalId = setInterval(() => {
|
||||
fetchTransactions(safeAddress)
|
||||
}, TIMEOUT)
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [safeAddress])
|
||||
|
||||
return (
|
||||
<TxsTable
|
||||
transactions={transactions}
|
||||
threshold={threshold}
|
||||
|
@ -38,7 +57,9 @@ const Transactions = ({
|
|||
safeAddress={safeAddress}
|
||||
createTransaction={createTransaction}
|
||||
processTransaction={processTransaction}
|
||||
nonce={nonce}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Transactions
|
||||
|
|
|
@ -18,7 +18,8 @@ export type Actions = {
|
|||
fetchTokens: typeof fetchTokens,
|
||||
processTransaction: typeof processTransaction,
|
||||
fetchEtherBalance: typeof fetchEtherBalance,
|
||||
activateTokensByBalance: typeof activateTokensByBalance
|
||||
activateTokensByBalance: typeof activateTokensByBalance,
|
||||
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -34,14 +34,12 @@ class SafeView extends React.Component<Props, State> {
|
|||
|
||||
componentDidMount() {
|
||||
const {
|
||||
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, fetchTransactions, safe,
|
||||
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, fetchTransactions,
|
||||
} = this.props
|
||||
|
||||
fetchSafe(safeUrl)
|
||||
fetchTokenBalances(safeUrl, activeTokens)
|
||||
if (safe && safe.address) {
|
||||
fetchTransactions(safe.address)
|
||||
}
|
||||
fetchTransactions(safeUrl)
|
||||
|
||||
// fetch tokens there to get symbols for tokens in TXs list
|
||||
fetchTokens()
|
||||
|
@ -52,12 +50,16 @@ class SafeView extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { activeTokens } = this.props
|
||||
const { activeTokens, safeUrl, fetchTransactions } = this.props
|
||||
const oldActiveTokensSize = prevProps.activeTokens.size
|
||||
|
||||
if (oldActiveTokensSize > 0 && activeTokens.size > oldActiveTokensSize) {
|
||||
this.checkForUpdates()
|
||||
}
|
||||
|
||||
if (safeUrl !== prevProps.safeUrl) {
|
||||
fetchTransactions(safeUrl)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -92,7 +94,11 @@ class SafeView extends React.Component<Props, State> {
|
|||
|
||||
checkForUpdates() {
|
||||
const {
|
||||
safeUrl, activeTokens, fetchTokenBalances, fetchEtherBalance, checkAndUpdateSafeOwners,
|
||||
safeUrl,
|
||||
activeTokens,
|
||||
fetchTokenBalances,
|
||||
fetchEtherBalance,
|
||||
checkAndUpdateSafeOwners,
|
||||
} = this.props
|
||||
checkAndUpdateSafeOwners(safeUrl)
|
||||
fetchTokenBalances(safeUrl, activeTokens)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
// @flow
|
||||
import axios from 'axios'
|
||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||
import { push } from 'connected-react-router'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { buildTxServiceUrl } from '~/logic/safe/transactions/txHistory'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import {
|
||||
getApprovalTransaction,
|
||||
|
@ -15,29 +17,68 @@ import {
|
|||
TX_TYPE_EXECUTION,
|
||||
saveTxToHistory,
|
||||
} from '~/logic/safe/transactions'
|
||||
import { type NotificationsQueue, getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
||||
import {
|
||||
type NotificationsQueue,
|
||||
getNotificationsFromTxType,
|
||||
showSnackbar,
|
||||
} from '~/logic/notifications'
|
||||
import { getErrorMessage } from '~/test/utils/ethereumErrors'
|
||||
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
|
||||
const createTransaction = (
|
||||
const getLastPendingTxNonce = async (safeAddress: string): Promise<number> => {
|
||||
let nonce
|
||||
|
||||
try {
|
||||
const url = buildTxServiceUrl(safeAddress)
|
||||
|
||||
const response = await axios.get(url, { params: { limit: 1 } })
|
||||
const lastTx = response.data.results[0]
|
||||
|
||||
nonce = lastTx.nonce + 1
|
||||
} catch (err) {
|
||||
// use current's safe nonce as fallback
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
nonce = (await safeInstance.nonce()).toString()
|
||||
}
|
||||
|
||||
return nonce
|
||||
}
|
||||
|
||||
type CreateTransactionArgs = {
|
||||
safeAddress: string,
|
||||
to: string,
|
||||
valueInWei: string,
|
||||
txData: string = EMPTY_DATA,
|
||||
txData: string,
|
||||
notifiedTransaction: NotifiedTransaction,
|
||||
enqueueSnackbar: Function,
|
||||
closeSnackbar: Function,
|
||||
shouldExecute?: boolean,
|
||||
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||
txNonce?: number,
|
||||
}
|
||||
|
||||
const createTransaction = ({
|
||||
safeAddress,
|
||||
to,
|
||||
valueInWei,
|
||||
txData = EMPTY_DATA,
|
||||
notifiedTransaction,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
shouldExecute = false,
|
||||
txNonce,
|
||||
}: CreateTransactionArgs) => async (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
getState: GetState<GlobalState>,
|
||||
) => {
|
||||
const state: GlobalState = getState()
|
||||
|
||||
dispatch(push(`${SAFELIST_ADDRESS}/${safeAddress}/transactions`))
|
||||
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const from = userAccountSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const threshold = await safeInstance.getThreshold()
|
||||
const nonce = (await safeInstance.nonce()).toString()
|
||||
const nonce = txNonce || await getLastPendingTxNonce(safeAddress)
|
||||
const isExecution = threshold.toNumber() === 1 || shouldExecute
|
||||
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
|
@ -46,8 +87,14 @@ const createTransaction = (
|
|||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
|
||||
const notificationsQueue: NotificationsQueue = getNotificationsFromTxType(notifiedTransaction)
|
||||
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
|
||||
const notificationsQueue: NotificationsQueue = getNotificationsFromTxType(
|
||||
notifiedTransaction,
|
||||
)
|
||||
const beforeExecutionKey = showSnackbar(
|
||||
notificationsQueue.beforeExecution,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
let pendingExecutionKey
|
||||
|
||||
let txHash
|
||||
|
@ -99,7 +146,11 @@ const createTransaction = (
|
|||
txHash = hash
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
|
||||
pendingExecutionKey = showSnackbar(
|
||||
notificationsQueue.pendingExecution,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
|
||||
try {
|
||||
await saveTxToHistory(
|
||||
|
@ -144,12 +195,32 @@ const createTransaction = (
|
|||
console.error(err)
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
|
||||
showSnackbar(
|
||||
notificationsQueue.afterExecutionError,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
|
||||
const executeDataUsedSignatures = safeInstance.contract.methods
|
||||
.execTransaction(to, valueInWei, txData, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|
||||
.execTransaction(
|
||||
to,
|
||||
valueInWei,
|
||||
txData,
|
||||
CALL,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS,
|
||||
sigs,
|
||||
)
|
||||
.encodeABI()
|
||||
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, from)
|
||||
const errMsg = await getErrorMessage(
|
||||
safeInstance.address,
|
||||
0,
|
||||
executeDataUsedSignatures,
|
||||
from,
|
||||
)
|
||||
console.error(`Error creating the TX: ${errMsg}`)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ export const buildSafe = async (safeAddress: string, safeName: string) => {
|
|||
const ethBalance = await getBalanceInEtherOf(safeAddress)
|
||||
|
||||
const threshold = Number(await gnosisSafe.getThreshold())
|
||||
const nonce = Number(await gnosisSafe.nonce())
|
||||
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), await getOwners(safeAddress)))
|
||||
|
||||
const safe: SafeProps = {
|
||||
|
@ -34,6 +35,7 @@ export const buildSafe = async (safeAddress: string, safeName: string) => {
|
|||
threshold,
|
||||
owners,
|
||||
ethBalance,
|
||||
nonce,
|
||||
}
|
||||
|
||||
return safe
|
||||
|
|
|
@ -6,7 +6,6 @@ import { type GlobalState } from '~/store/index'
|
|||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { loadSafeSubjects } from '~/utils/storage/transactions'
|
||||
import { buildTxServiceUrl, type TxServiceType } from '~/logic/safe/transactions/txHistory'
|
||||
import { getOwners } from '~/logic/safe/utils'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
@ -50,9 +49,7 @@ type TxServiceModel = {
|
|||
export const buildTransactionFrom = async (
|
||||
safeAddress: string,
|
||||
tx: TxServiceModel,
|
||||
safeSubjects: Map<string, string>,
|
||||
) => {
|
||||
const name = safeSubjects.get(String(tx.nonce)) || 'Unknown'
|
||||
const storedOwners = await getOwners(safeAddress)
|
||||
const confirmations = List(
|
||||
tx.confirmations.map((conf: ConfirmationServiceModel) => {
|
||||
|
@ -123,7 +120,6 @@ export const buildTransactionFrom = async (
|
|||
}
|
||||
|
||||
return makeTransaction({
|
||||
name,
|
||||
symbol,
|
||||
nonce: tx.nonce,
|
||||
value: tx.value.toString(),
|
||||
|
@ -189,9 +185,9 @@ export const loadSafeTransactions = async (safeAddress: string) => {
|
|||
} catch (err) {
|
||||
console.error(`Requests for transactions for ${safeAddress} failed with 404`, err)
|
||||
}
|
||||
const safeSubjects = loadSafeSubjects(safeAddress)
|
||||
|
||||
const txsRecord = await Promise.all(
|
||||
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx, safeSubjects)),
|
||||
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx)),
|
||||
)
|
||||
return Map().set(safeAddress, List(txsRecord))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { List } from 'immutable'
|
||||
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
|
@ -53,7 +54,7 @@ export const generateSignaturesFromTxConfirmations = (
|
|||
return sigs
|
||||
}
|
||||
|
||||
const processTransaction = (
|
||||
type ProcessTransactionArgs = {
|
||||
safeAddress: string,
|
||||
tx: Transaction,
|
||||
userAddress: string,
|
||||
|
@ -61,7 +62,20 @@ const processTransaction = (
|
|||
enqueueSnackbar: Function,
|
||||
closeSnackbar: Function,
|
||||
approveAndExecute?: boolean,
|
||||
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||
}
|
||||
|
||||
const processTransaction = ({
|
||||
safeAddress,
|
||||
tx,
|
||||
userAddress,
|
||||
notifiedTransaction,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
approveAndExecute,
|
||||
}: ProcessTransactionArgs) => async (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
getState: Function,
|
||||
) => {
|
||||
const state: GlobalState = getState()
|
||||
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
@ -69,7 +83,10 @@ const processTransaction = (
|
|||
const threshold = (await safeInstance.getThreshold()).toNumber()
|
||||
const shouldExecute = threshold === tx.confirmations.size || approveAndExecute
|
||||
|
||||
let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, approveAndExecute && userAddress)
|
||||
let sigs = generateSignaturesFromTxConfirmations(
|
||||
tx.confirmations,
|
||||
approveAndExecute && userAddress,
|
||||
)
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
if (!sigs) {
|
||||
sigs = `0x000000000000000000000000${from.replace(
|
||||
|
@ -78,8 +95,14 @@ const processTransaction = (
|
|||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
}
|
||||
|
||||
const notificationsQueue: NotificationsQueue = getNotificationsFromTxType(notifiedTransaction)
|
||||
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
|
||||
const notificationsQueue: NotificationsQueue = getNotificationsFromTxType(
|
||||
notifiedTransaction,
|
||||
)
|
||||
const beforeExecutionKey = showSnackbar(
|
||||
notificationsQueue.beforeExecution,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
let pendingExecutionKey
|
||||
|
||||
let txHash
|
||||
|
@ -130,7 +153,11 @@ const processTransaction = (
|
|||
txHash = hash
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
|
||||
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
|
||||
pendingExecutionKey = showSnackbar(
|
||||
notificationsQueue.pendingExecution,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
|
||||
try {
|
||||
await saveTxToHistory(
|
||||
|
@ -169,16 +196,31 @@ const processTransaction = (
|
|||
)
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
if (shouldExecute) {
|
||||
dispatch(fetchSafe(safeAddress))
|
||||
}
|
||||
|
||||
return receipt.transactionHash
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
closeSnackbar(beforeExecutionKey)
|
||||
closeSnackbar(pendingExecutionKey)
|
||||
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
|
||||
showSnackbar(
|
||||
notificationsQueue.afterExecutionError,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
)
|
||||
|
||||
const executeData = safeInstance.contract.methods.approveHash(txHash).encodeABI()
|
||||
const errMsg = await getErrorMessage(safeInstance.address, 0, executeData, from)
|
||||
const executeData = safeInstance.contract.methods
|
||||
.approveHash(txHash)
|
||||
.encodeABI()
|
||||
const errMsg = await getErrorMessage(
|
||||
safeInstance.address,
|
||||
0,
|
||||
executeData,
|
||||
from,
|
||||
)
|
||||
console.error(`Error executing the TX: ${errMsg}`)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ export type SafeProps = {
|
|||
activeTokens: Set<string>,
|
||||
blacklistedTokens: Set<string>,
|
||||
ethBalance?: string,
|
||||
nonce: number,
|
||||
}
|
||||
|
||||
const SafeRecord: RecordFactory<SafeProps> = Record({
|
||||
|
@ -25,6 +26,7 @@ const SafeRecord: RecordFactory<SafeProps> = Record({
|
|||
activeTokens: new Set(),
|
||||
blacklistedTokens: new Set(),
|
||||
balances: Map({}),
|
||||
nonce: 0,
|
||||
})
|
||||
|
||||
export type Safe = RecordOf<SafeProps>
|
||||
|
|
|
@ -15,10 +15,3 @@ export const storeSubject = async (safeAddress: string, nonce: number, subject:
|
|||
console.error('Error storing transaction subject in localstorage', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadSafeSubjects = (safeAddress: string): Map<string, string> => {
|
||||
const key = getSubjectKeyFrom(safeAddress)
|
||||
const data: any = loadFromStorage(key)
|
||||
|
||||
return data ? Map(data) : Map()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue