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