Feature #180: Predict transaction nonce (#293)

* 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:
Mikhail Mikheev 2019-12-12 12:30:27 +04:00 committed by GitHub
parent 87a7796a84
commit bc7d5836f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 543 additions and 2569 deletions

View File

@ -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

View File

@ -13,7 +13,7 @@ type Props = {
margin?: Size,
padding?: Size,
justify?: 'center' | 'right' | 'left' | 'space-around',
children: React.Node,
children?: React.Node,
className?: string,
}

View File

@ -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;
}
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -176,6 +176,7 @@ const Layout = (props: Props) => {
<Transactions
threshold={safe.threshold}
owners={safe.owners}
nonce={safe.nonce}
transactions={transactions}
fetchTransactions={fetchTransactions}
safeAddress={address}

View File

@ -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 })

View File

@ -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 })

View File

@ -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({

View File

@ -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 (

View File

@ -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"
/>
</>

View File

@ -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()
}

View File

@ -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

View File

@ -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 && (

View File

@ -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>
)

View File

@ -53,6 +53,9 @@ export const styles = () => ({
textTransform: 'uppercase',
letterSpacing: '1px',
},
olderTxAnnotation: {
textAlign: 'center',
},
ownerListTitleDone: {
color: secondary,
},

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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}`)
}

View File

@ -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

View File

@ -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))
}

View File

@ -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}`)
}

View File

@ -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>

View File

@ -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()
}

2646
yarn.lock

File diff suppressed because it is too large Load Diff