Merge branch 'development' of https://github.com/gnosis/safe-react into 189-cookie-banner

This commit is contained in:
apane 2019-11-25 10:16:31 -03:00
commit 3b063d3ee8
27 changed files with 291 additions and 74 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "safe-react", "name": "safe-react",
"version": "1.0.1", "version": "1.4.3",
"description": "Allowing crypto users manage funds in a safer way", "description": "Allowing crypto users manage funds in a safer way",
"homepage": "https://github.com/gnosis/safe-react#readme", "homepage": "https://github.com/gnosis/safe-react#readme",
"bugs": { "bugs": {
@ -55,7 +55,7 @@
"qrcode.react": "1.0.0", "qrcode.react": "1.0.0",
"react": "16.12.0", "react": "16.12.0",
"react-dom": "16.12.0", "react-dom": "16.12.0",
"react-final-form": "6.3.2", "react-final-form": "6.3.3",
"react-final-form-listeners": "^1.0.2", "react-final-form-listeners": "^1.0.2",
"react-hot-loader": "4.12.18", "react-hot-loader": "4.12.18",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",

View File

@ -20,6 +20,7 @@ const styles = () => ({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
display: 'flex', display: 'flex',
overflowY: 'scroll',
}, },
paper: { paper: {
position: 'absolute', position: 'absolute',

View File

@ -24,7 +24,7 @@ export const cellWidth = (width: number | typeof undefined) => {
} }
return { return {
width: `${width}px`, maxWidth: `${width}px`,
} }
} }

View File

@ -15,6 +15,11 @@ export const getApprovalTransaction = async (
data: string, data: string,
operation: Operation, operation: Operation,
nonce: number, nonce: number,
safeTxGas: number,
baseGas: number,
gasPrice: number,
gasToken: string,
refundReceiver: string,
sender: string, sender: string,
) => { ) => {
const txHash = await safeInstance.getTransactionHash( const txHash = await safeInstance.getTransactionHash(
@ -22,11 +27,11 @@ export const getApprovalTransaction = async (
valueInWei, valueInWei,
data, data,
operation, operation,
0, safeTxGas,
0, baseGas,
0, gasPrice,
ZERO_ADDRESS, gasToken,
ZERO_ADDRESS, refundReceiver,
nonce, nonce,
{ {
from: sender, from: sender,
@ -40,7 +45,6 @@ export const getApprovalTransaction = async (
return contract.methods.approveHash(txHash) return contract.methods.approveHash(txHash)
} catch (err) { } catch (err) {
console.error(`Error while approving transaction: ${err}`) console.error(`Error while approving transaction: ${err}`)
throw err throw err
} }
} }
@ -52,6 +56,11 @@ export const getExecutionTransaction = async (
data: string, data: string,
operation: Operation, operation: Operation,
nonce: string | number, nonce: string | number,
safeTxGas: string | number,
baseGas: string | number,
gasPrice: string | number,
gasToken: string,
refundReceiver: string,
sender: string, sender: string,
sigs: string, sigs: string,
) => { ) => {
@ -59,7 +68,7 @@ export const getExecutionTransaction = async (
const web3 = getWeb3() const web3 = getWeb3()
const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address) const contract = new web3.eth.Contract(GnosisSafeSol.abi, safeInstance.address)
return contract.methods.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs) return contract.methods.execTransaction(to, valueInWei, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, sigs)
} catch (err) { } catch (err) {
console.error(`Error while creating transaction: ${err}`) console.error(`Error while creating transaction: ${err}`)

View File

@ -2,7 +2,6 @@
import axios from 'axios' import axios from 'axios'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { getTxServiceUriFrom, getTxServiceHost } from '~/config' import { getTxServiceUriFrom, getTxServiceHost } from '~/config'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
export type TxServiceType = 'confirmation' | 'execution' | 'initialised' export type TxServiceType = 'confirmation' | 'execution' | 'initialised'
export type Operation = 0 | 1 | 2 export type Operation = 0 | 1 | 2
@ -14,6 +13,11 @@ const calculateBodyFrom = async (
data: string, data: string,
operation: Operation, operation: Operation,
nonce: string | number, nonce: string | number,
safeTxGas: string | number,
baseGas: string | number,
gasPrice: string | number,
gasToken: string,
refundReceiver: string,
transactionHash: string, transactionHash: string,
sender: string, sender: string,
confirmationType: TxServiceType, confirmationType: TxServiceType,
@ -23,11 +27,11 @@ const calculateBodyFrom = async (
valueInWei, valueInWei,
data, data,
operation, operation,
0, safeTxGas,
0, baseGas,
0, gasPrice,
ZERO_ADDRESS, gasToken,
ZERO_ADDRESS, refundReceiver,
nonce, nonce,
) )
@ -37,11 +41,11 @@ const calculateBodyFrom = async (
data, data,
operation, operation,
nonce, nonce,
safeTxGas: 0, safeTxGas,
baseGas: 0, baseGas,
gasPrice: 0, gasPrice,
gasToken: ZERO_ADDRESS, gasToken,
refundReceiver: ZERO_ADDRESS, refundReceiver,
contractTransactionHash, contractTransactionHash,
transactionHash, transactionHash,
sender: getWeb3().utils.toChecksumAddress(sender), sender: getWeb3().utils.toChecksumAddress(sender),
@ -63,12 +67,32 @@ export const saveTxToHistory = async (
data: string, data: string,
operation: Operation, operation: Operation,
nonce: number | string, nonce: number | string,
safeTxGas: string | number,
baseGas: string | number,
gasPrice: string | number,
gasToken: string,
refundReceiver: string,
txHash: string, txHash: string,
sender: string, sender: string,
type: TxServiceType, type: TxServiceType,
) => { ) => {
const url = buildTxServiceUrl(safeInstance.address) const url = buildTxServiceUrl(safeInstance.address)
const body = await calculateBodyFrom(safeInstance, to, valueInWei, data, operation, nonce, txHash, sender, type) const body = await calculateBodyFrom(
safeInstance,
to,
valueInWei,
data,
operation,
nonce,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
txHash,
sender,
type,
)
const response = await axios.post(url, body) const response = await axios.post(url, body)
if (response.status !== 202) { if (response.status !== 202) {

View File

@ -16,5 +16,7 @@ export const sameAddress = (firstAddress: string, secondAddress: string): boolea
export const shortVersionOf = (address: string, cut: number) => { export const shortVersionOf = (address: string, cut: number) => {
const final = 42 - cut const final = 42 - cut
if (!address) return 'Unknown address'
if (address.length < final) return address
return `${address.substring(0, cut)}...${address.substring(final)}` return `${address.substring(0, cut)}...${address.substring(final)}`
} }

View File

@ -196,6 +196,7 @@ const Layout = (props: Props) => {
network={network} network={network}
userAddress={userAddress} userAddress={userAddress}
createTransaction={createTransaction} createTransaction={createTransaction}
safe={safe}
/> />
)} )}
/> />

View File

@ -10,6 +10,7 @@ import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import CheckOwner from './screens/CheckOwner' import CheckOwner from './screens/CheckOwner'
import ThresholdForm from './screens/ThresholdForm' import ThresholdForm from './screens/ThresholdForm'
import ReviewRemoveOwner from './screens/Review' import ReviewRemoveOwner from './screens/Review'
import type { Safe } from '~/routes/safe/store/models/safe'
const styles = () => ({ const styles = () => ({
biggerModalWindow: { biggerModalWindow: {
@ -34,6 +35,7 @@ type Props = {
removeSafeOwner: Function, removeSafeOwner: Function,
enqueueSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function, closeSnackbar: Function,
safe: Safe
} }
type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner' type ActiveScreen = 'checkOwner' | 'selectThreshold' | 'reviewRemoveOwner'
@ -48,6 +50,7 @@ export const sendRemoveOwner = async (
closeSnackbar: Function, closeSnackbar: Function,
createTransaction: Function, createTransaction: Function,
removeSafeOwner: Function, removeSafeOwner: Function,
safe: Safe,
) => { ) => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const safeOwners = await gnosisSafe.getOwners() const safeOwners = await gnosisSafe.getOwners()
@ -69,7 +72,7 @@ export const sendRemoveOwner = async (
closeSnackbar, closeSnackbar,
) )
if (txHash) { if (txHash && safe.threshold === 1) {
removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove }) removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })
} }
} }
@ -88,6 +91,7 @@ const RemoveOwner = ({
removeSafeOwner, removeSafeOwner,
enqueueSnackbar, enqueueSnackbar,
closeSnackbar, closeSnackbar,
safe,
}: Props) => { }: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner') const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({}) const [values, setValues] = useState<Object>({})
@ -130,6 +134,7 @@ const RemoveOwner = ({
closeSnackbar, closeSnackbar,
createTransaction, createTransaction,
removeSafeOwner, removeSafeOwner,
safe,
) )
} }

View File

@ -9,6 +9,7 @@ import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from '~/logic/contracts/safeContracts'
import OwnerForm from './screens/OwnerForm' import OwnerForm from './screens/OwnerForm'
import ReviewReplaceOwner from './screens/Review' import ReviewReplaceOwner from './screens/Review'
import type { Safe } from '~/routes/safe/store/models/safe'
const styles = () => ({ const styles = () => ({
biggerModalWindow: { biggerModalWindow: {
@ -33,6 +34,7 @@ type Props = {
replaceSafeOwner: Function, replaceSafeOwner: Function,
enqueueSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function, closeSnackbar: Function,
safe: Safe,
} }
type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner' type ActiveScreen = 'checkOwner' | 'reviewReplaceOwner'
@ -44,6 +46,7 @@ export const sendReplaceOwner = async (
closeSnackbar: Function, closeSnackbar: Function,
createTransaction: Function, createTransaction: Function,
replaceSafeOwner: Function, replaceSafeOwner: Function,
safe: Safe,
) => { ) => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const safeOwners = await gnosisSafe.getOwners() const safeOwners = await gnosisSafe.getOwners()
@ -65,7 +68,7 @@ export const sendReplaceOwner = async (
closeSnackbar, closeSnackbar,
) )
if (txHash) { if (txHash && safe.threshold === 1) {
replaceSafeOwner({ replaceSafeOwner({
safeAddress, safeAddress,
oldOwnerAddress: ownerAddressToRemove, oldOwnerAddress: ownerAddressToRemove,
@ -89,6 +92,7 @@ const ReplaceOwner = ({
replaceSafeOwner, replaceSafeOwner,
enqueueSnackbar, enqueueSnackbar,
closeSnackbar, closeSnackbar,
safe,
}: Props) => { }: Props) => {
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner') const [activeScreen, setActiveScreen] = useState<ActiveScreen>('checkOwner')
const [values, setValues] = useState<Object>({}) const [values, setValues] = useState<Object>({})
@ -121,6 +125,7 @@ const ReplaceOwner = ({
closeSnackbar, closeSnackbar,
createTransaction, createTransaction,
replaceSafeOwner, replaceSafeOwner,
safe,
) )
} catch (error) { } catch (error) {
console.error('Error while removing an owner', error) console.error('Error while removing an owner', error)

View File

@ -32,6 +32,7 @@ import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
import RenameOwnerIcon from './assets/icons/rename-owner.svg' import RenameOwnerIcon from './assets/icons/rename-owner.svg'
import RemoveOwnerIcon from '../assets/icons/bin.svg' import RemoveOwnerIcon from '../assets/icons/bin.svg'
import Paragraph from '~/components/layout/Paragraph/index' import Paragraph from '~/components/layout/Paragraph/index'
import type { Safe } from '~/routes/safe/store/models/safe'
export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn'
export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn' export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn'
@ -53,6 +54,7 @@ type Props = {
replaceSafeOwner: Function, replaceSafeOwner: Function,
editSafeOwner: Function, editSafeOwner: Function,
granted: boolean, granted: boolean,
safe: Safe
} }
type State = { type State = {
@ -111,6 +113,7 @@ class ManageOwners extends React.Component<Props, State> {
replaceSafeOwner, replaceSafeOwner,
editSafeOwner, editSafeOwner,
granted, granted,
safe,
} = this.props } = this.props
const { const {
showAddOwner, showAddOwner,
@ -237,6 +240,7 @@ class ManageOwners extends React.Component<Props, State> {
userAddress={userAddress} userAddress={userAddress}
createTransaction={createTransaction} createTransaction={createTransaction}
removeSafeOwner={removeSafeOwner} removeSafeOwner={removeSafeOwner}
safe={safe}
/> />
<ReplaceOwnerModal <ReplaceOwnerModal
onClose={this.onHide('ReplaceOwner')} onClose={this.onHide('ReplaceOwner')}
@ -251,6 +255,7 @@ class ManageOwners extends React.Component<Props, State> {
userAddress={userAddress} userAddress={userAddress}
createTransaction={createTransaction} createTransaction={createTransaction}
replaceSafeOwner={replaceSafeOwner} replaceSafeOwner={replaceSafeOwner}
safe={safe}
/> />
<EditOwnerModal <EditOwnerModal
onClose={this.onHide('EditOwner')} onClose={this.onHide('EditOwner')}

View File

@ -19,6 +19,7 @@ import ManageOwners from './ManageOwners'
import actions, { type Actions } from './actions' import actions, { type Actions } from './actions'
import { styles } from './style' import { styles } from './style'
import RemoveSafeIcon from './assets/icons/bin.svg' import RemoveSafeIcon from './assets/icons/bin.svg'
import type { Safe } from '~/routes/safe/store/models/safe'
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab' export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
@ -43,6 +44,7 @@ type Props = Actions & {
replaceSafeOwner: Function, replaceSafeOwner: Function,
editSafeOwner: Function, editSafeOwner: Function,
userAddress: string, userAddress: string,
safe: Safe
} }
type Action = 'RemoveSafe' type Action = 'RemoveSafe'
@ -87,6 +89,7 @@ class Settings extends React.Component<Props, State> {
removeSafeOwner, removeSafeOwner,
replaceSafeOwner, replaceSafeOwner,
editSafeOwner, editSafeOwner,
safe,
} = this.props } = this.props
return ( return (
@ -152,6 +155,7 @@ class Settings extends React.Component<Props, State> {
replaceSafeOwner={replaceSafeOwner} replaceSafeOwner={replaceSafeOwner}
editSafeOwner={editSafeOwner} editSafeOwner={editSafeOwner}
granted={granted} granted={granted}
safe={safe}
/> />
)} )}
{menuOptionIndex === 3 && ( {menuOptionIndex === 3 && (

View File

@ -64,10 +64,10 @@ const ApproveTxModal = ({
enqueueSnackbar, enqueueSnackbar,
closeSnackbar, closeSnackbar,
}: Props) => { }: Props) => {
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(true) const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
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 = tx.confirmations.size + 1 === threshold
useEffect(() => { useEffect(() => {
let isCurrent = true let isCurrent = true
@ -107,7 +107,7 @@ const ApproveTxModal = ({
TX_NOTIFICATION_TYPES.CONFIRMATION_TX, TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
enqueueSnackbar, enqueueSnackbar,
closeSnackbar, closeSnackbar,
approveAndExecute, approveAndExecute && oneConfirmationLeft,
) )
onClose() onClose()
} }
@ -131,7 +131,7 @@ const ApproveTxModal = ({
<br /> <br />
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold> <Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
</Paragraph> </Paragraph>
{!thresholdReached && oneConfirmationLeft && ( {oneConfirmationLeft && (
<> <>
<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 approve but execute the transaction

View File

@ -54,6 +54,9 @@ export const getTxData = (tx: Transaction): DecodedTxData => {
} }
} else if (tx.cancellationTx) { } else if (tx.cancellationTx) {
txData.cancellationTx = true txData.cancellationTx = true
} else {
txData.recipient = tx.recipient
txData.value = 0
} }
return txData return txData

View File

@ -98,6 +98,26 @@ const ExpandedTx = ({
{formatDate(tx.executionDate)} {formatDate(tx.executionDate)}
</Paragraph> </Paragraph>
)} )}
{tx.refundParams && (
<Paragraph noMargin>
<Bold>TX refund: </Bold>
max.
{' '}
{tx.refundParams.fee}
{' '}
{tx.refundParams.symbol}
</Paragraph>
)}
{tx.operation === 1 && (
<Paragraph noMargin>
<Bold>Delegate Call</Bold>
</Paragraph>
)}
{tx.operation === 2 && (
<Paragraph noMargin>
<Bold>Contract Creation</Bold>
</Paragraph>
)}
</Block> </Block>
<Hairline /> <Hairline />
<TxDescription tx={tx} /> <TxDescription tx={tx} />

View File

@ -1,6 +1,7 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import CircularProgress from '@material-ui/core/CircularProgress'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Paragraph from '~/components/layout/Paragraph/' import Paragraph from '~/components/layout/Paragraph/'
import Img from '~/components/layout/Img' import Img from '~/components/layout/Img'
@ -20,6 +21,7 @@ const statusToIcon = {
cancelled: ErrorIcon, cancelled: ErrorIcon,
awaiting_confirmations: AwaitingIcon, awaiting_confirmations: AwaitingIcon,
awaiting_execution: AwaitingIcon, awaiting_execution: AwaitingIcon,
pending: <CircularProgress size={14} />,
} }
const statusToLabel = { const statusToLabel = {
@ -27,6 +29,7 @@ const statusToLabel = {
cancelled: 'Cancelled', cancelled: 'Cancelled',
awaiting_confirmations: 'Awaiting', awaiting_confirmations: 'Awaiting',
awaiting_execution: 'Awaiting', awaiting_execution: 'Awaiting',
pending: 'Pending',
} }
const statusIconStyle = { const statusIconStyle = {
@ -34,11 +37,21 @@ const statusIconStyle = {
width: '14px', width: '14px',
} }
const Status = ({ classes, status }: Props) => ( const Status = ({ classes, status }: Props) => {
<Block className={`${classes.container} ${classes[status]}`}> const Icon = statusToIcon[status]
<Img src={statusToIcon[status]} alt="OK Icon" style={statusIconStyle} />
<Paragraph noMargin className={classes.statusText}>{statusToLabel[status]}</Paragraph> return (
</Block> <Block className={`${classes.container} ${classes[status]}`}>
) {typeof Icon === 'object' ? (
Icon
) : (
<Img src={Icon} alt="OK Icon" style={statusIconStyle} />
)}
<Paragraph noMargin className={classes.statusText}>
{statusToLabel[status]}
</Paragraph>
</Block>
)
}
export default withStyles(styles)(Status) export default withStyles(styles)(Status)

View File

@ -29,6 +29,10 @@ export const styles = () => ({
backgroundColor: '#dfebff', backgroundColor: '#dfebff',
color: disabled, color: disabled,
}, },
pending: {
backgroundColor: '#fff3e2',
color: '#e8673c',
},
statusText: { statusText: {
marginLeft: 'auto', marginLeft: 'auto',
textTransform: 'uppercase', textTransform: 'uppercase',

View File

@ -1,5 +1,5 @@
// @flow // @flow
import fetchSafe from '~/routes/safe/store/actions/fetchSafe' import fetchSafe, { checkAndUpdateSafeOwners } from '~/routes/safe/store/actions/fetchSafe'
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances' import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance' import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance'
import createTransaction from '~/routes/safe/store/actions/createTransaction' import createTransaction from '~/routes/safe/store/actions/createTransaction'
@ -28,4 +28,5 @@ export default {
fetchTransactions, fetchTransactions,
updateSafe, updateSafe,
fetchEtherBalance, fetchEtherBalance,
checkAndUpdateSafeOwners,
} }

View File

@ -89,9 +89,9 @@ class SafeView extends React.Component<Props, State> {
checkForUpdates() { checkForUpdates() {
const { const {
safeUrl, activeTokens, fetchTokenBalances, fetchEtherBalance, safeUrl, activeTokens, fetchTokenBalances, fetchEtherBalance, checkAndUpdateSafeOwners,
} = this.props } = this.props
checkAndUpdateSafeOwners(safeUrl)
fetchTokenBalances(safeUrl, activeTokens) fetchTokenBalances(safeUrl, activeTokens)
fetchEtherBalance(safeUrl) fetchEtherBalance(safeUrl)
} }

View File

@ -1,7 +1,6 @@
// @flow // @flow
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { createSelector, createStructuredSelector, type Selector } from 'reselect' import { createSelector, createStructuredSelector, type Selector } from 'reselect'
import { isAfter, parseISO } from 'date-fns'
import { import {
safeSelector, safeSelector,
safeActiveTokensSelector, safeActiveTokensSelector,
@ -41,6 +40,8 @@ const getTxStatus = (tx: Transaction, safe: Safe): TransactionStatus => {
txStatus = 'cancelled' txStatus = 'cancelled'
} else if (tx.confirmations.size === safe.threshold) { } else if (tx.confirmations.size === safe.threshold) {
txStatus = 'awaiting_execution' txStatus = 'awaiting_execution'
} else if (!tx.confirmations.size) {
txStatus = 'pending'
} }
return txStatus return txStatus
@ -115,9 +116,7 @@ const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Tran
let replacementTransaction let replacementTransaction
if (!tx.isExecuted) { if (!tx.isExecuted) {
replacementTransaction = transactions.findLast( replacementTransaction = transactions.findLast(
(transaction) => (transaction.nonce === tx.nonce (transaction) => transaction.isExecuted && transaction.nonce >= tx.nonce,
&& isAfter(parseISO(transaction.submissionDate), parseISO(tx.submissionDate)))
|| transaction.nonce > tx.nonce,
) )
if (replacementTransaction) { if (replacementTransaction) {
extendedTx = tx.set('cancelled', true) extendedTx = tx.set('cancelled', true)

View File

@ -54,9 +54,15 @@ const createTransaction = (
let tx let tx
try { try {
if (isExecution) { if (isExecution) {
tx = await getExecutionTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from, sigs) tx = await getExecutionTransaction(
safeInstance, to, valueInWei, txData, CALL, nonce,
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs
)
} else { } else {
tx = await getApprovalTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) tx = await getApprovalTransaction(
safeInstance, to, valueInWei, txData, CALL, nonce,
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs
)
} }
const sendParams = { from, value: 0 } const sendParams = { from, value: 0 }
@ -81,10 +87,16 @@ const createTransaction = (
txData, txData,
CALL, CALL,
nonce, nonce,
0,
0,
0,
ZERO_ADDRESS,
ZERO_ADDRESS,
txHash, txHash,
from, from,
isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION, isExecution ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
) )
dispatch(fetchTransactions(safeAddress))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
@ -94,7 +106,6 @@ const createTransaction = (
}) })
.then((receipt) => { .then((receipt) => {
closeSnackbar(pendingExecutionKey) closeSnackbar(pendingExecutionKey)
showSnackbar( showSnackbar(
isExecution isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded ? notificationsQueue.afterExecution.noMoreConfirmationsNeeded

View File

@ -5,9 +5,12 @@ import { type GlobalState } from '~/store/index'
import { makeOwner } from '~/routes/safe/store/models/owner' import { makeOwner } from '~/routes/safe/store/models/owner'
import type { SafeProps } from '~/routes/safe/store/models/safe' import type { SafeProps } from '~/routes/safe/store/models/safe'
import addSafe from '~/routes/safe/store/actions/addSafe' import addSafe from '~/routes/safe/store/actions/addSafe'
import { getOwners, getSafeName } from '~/logic/safe/utils' import { getOwners, getSafeName, SAFES_KEY } from '~/logic/safe/utils'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3' import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import { loadFromStorage } from '~/utils/storage'
import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner'
import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner'
const buildOwnersFrom = ( const buildOwnersFrom = (
safeOwners: string[], safeOwners: string[],
@ -35,6 +38,36 @@ export const buildSafe = async (safeAddress: string, safeName: string) => {
return safe return safe
} }
const getLocalSafe = async (safeAddress: string) => {
const storedSafes = (await loadFromStorage(SAFES_KEY)) || {}
return storedSafes[safeAddress]
}
export const checkAndUpdateSafeOwners = (safeAddress: string) => async (
dispatch: ReduxDispatch<GlobalState>,
) => {
// Check if the owner's safe did change and update them
const [gnosisSafe, localSafe] = await Promise.all([getGnosisSafeInstanceAt(safeAddress), getLocalSafe(safeAddress)])
const remoteOwners = await gnosisSafe.getOwners()
// Converts from [ { address, ownerName} ] to address array
const localOwners = localSafe.owners.map((localOwner) => localOwner.address)
// If the remote owners does not contain a local address, we remove that local owner
localOwners.forEach((localAddress) => {
if (!remoteOwners.includes(localAddress)) {
dispatch(removeSafeOwner({ safeAddress, ownerAddress: localAddress }))
}
})
// If the remote has an owner that we don't have locally, we add it
remoteOwners.forEach((remoteAddress) => {
if (!localOwners.includes(remoteAddress)) {
dispatch(addSafeOwner({ safeAddress, ownerAddress: remoteAddress, ownerName: 'UNKNOWN' }))
}
})
}
// eslint-disable-next-line consistent-return
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => { export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
try { try {
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE' const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'

View File

@ -14,9 +14,9 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { addTransactions } from './addTransactions' import { addTransactions } from './addTransactions'
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers' import { isTokenTransfer } from '~/logic/tokens/utils/tokenHelpers'
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions'
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds' import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi' import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
let web3 let web3
@ -33,11 +33,17 @@ type TxServiceModel = {
data: string, data: string,
operation: number, operation: number,
nonce: number, nonce: number,
safeTxGas: number,
baseGas: number,
gasPrice: number,
gasToken: string,
refundReceiver: string,
safeTxHash: string, safeTxHash: string,
submissionDate: string, submissionDate: string,
executionDate: string, executionDate: string,
confirmations: ConfirmationServiceModel[], confirmations: ConfirmationServiceModel[],
isExecuted: boolean, isExecuted: boolean,
transactionHash: string,
} }
export const buildTransactionFrom = async ( export const buildTransactionFrom = async (
@ -55,6 +61,7 @@ export const buildTransactionFrom = async (
owner: makeOwner({ address: conf.owner, name: ownerName }), owner: makeOwner({ address: conf.owner, name: ownerName }),
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType), type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
hash: conf.transactionHash, hash: conf.transactionHash,
signature: conf.signature,
}) })
}), }),
) )
@ -63,11 +70,25 @@ export const buildTransactionFrom = async (
const isSendTokenTx = await isTokenTransfer(tx.data, tx.value) const isSendTokenTx = await isTokenTransfer(tx.data, tx.value)
const customTx = tx.to !== safeAddress && !!tx.data && !isSendTokenTx const customTx = tx.to !== safeAddress && !!tx.data && !isSendTokenTx
let executionTxHash let refundParams = null
const executionTx = confirmations.find((conf) => conf.type === TX_TYPE_EXECUTION) if (tx.gasPrice > 0) {
let refundSymbol = 'ETH'
let decimals = 18
if (tx.gasToken !== ZERO_ADDRESS) {
const gasToken = await (await getHumanFriendlyToken()).at(tx.gasToken)
refundSymbol = await gasToken.symbol()
decimals = await gasToken.decimals()
}
if (executionTx) { const feeString = (tx.gasPrice * (tx.baseGas + tx.safeTxGas)).toString().padStart(decimals, 0)
executionTxHash = executionTx.hash const whole = feeString.slice(0, feeString.length - decimals) || '0'
const fraction = feeString.slice(feeString.length - decimals)
const formattedFee = `${whole}.${fraction}`
refundParams = {
fee: formattedFee,
symbol: refundSymbol,
}
} }
let symbol = 'ETH' let symbol = 'ETH'
@ -109,10 +130,17 @@ export const buildTransactionFrom = async (
decimals, decimals,
recipient: tx.to, recipient: tx.to,
data: tx.data ? tx.data : EMPTY_DATA, data: tx.data ? tx.data : EMPTY_DATA,
operation: tx.operation,
safeTxGas: tx.safeTxGas,
baseGas: tx.baseGas,
gasPrice: tx.gasPrice,
gasToken: tx.gasToken,
refundReceiver: tx.refundReceiver,
refundParams,
isExecuted: tx.isExecuted, isExecuted: tx.isExecuted,
submissionDate: tx.submissionDate, submissionDate: tx.submissionDate,
executionDate: tx.executionDate, executionDate: tx.executionDate,
executionTxHash, executionTxHash: tx.transactionHash,
safeTxHash: tx.safeTxHash, safeTxHash: tx.safeTxHash,
isTokenTransfer: isSendTokenTx, isTokenTransfer: isSendTokenTx,
decodedParams, decodedParams,

View File

@ -11,7 +11,6 @@ import {
type NotifiedTransaction, type NotifiedTransaction,
getApprovalTransaction, getApprovalTransaction,
getExecutionTransaction, getExecutionTransaction,
CALL,
saveTxToHistory, saveTxToHistory,
TX_TYPE_EXECUTION, TX_TYPE_EXECUTION,
TX_TYPE_CONFIRMATION, TX_TYPE_CONFIRMATION,
@ -27,18 +26,27 @@ export const generateSignaturesFromTxConfirmations = (
) => { ) => {
// The constant parts need to be sorted so that the recovered signers are sorted ascending // The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed). // (natural order) by address (not checksummed).
let confirmedAdresses = confirmations.map((conf) => conf.owner.address) const confirmationsMap = confirmations.reduce((map, obj) => {
map[obj.owner.address] = obj // eslint-disable-line no-param-reassign
return map
}, {})
if (preApprovingOwner) { if (preApprovingOwner) {
confirmedAdresses = confirmedAdresses.push(preApprovingOwner) confirmationsMap[preApprovingOwner] = { owner: preApprovingOwner }
} }
let sigs = '0x' let sigs = '0x'
confirmedAdresses.sort().forEach((addr) => { Object.keys(confirmationsMap).sort().forEach((addr) => {
sigs += `000000000000000000000000${addr.replace( const conf = confirmationsMap[addr]
'0x', if (conf.signature) {
'', sigs += conf.signature.slice(2)
)}000000000000000000000000000000000000000000000000000000000000000001` } else {
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
sigs += `000000000000000000000000${addr.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
}) })
return sigs return sigs
} }
@ -56,7 +64,6 @@ const processTransaction = (
const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const from = userAccountSelector(state) const from = userAccountSelector(state)
const nonce = (await safeInstance.nonce()).toString()
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
@ -82,13 +89,31 @@ const processTransaction = (
tx.recipient, tx.recipient,
tx.value, tx.value,
tx.data, tx.data,
CALL, tx.operation,
nonce, tx.nonce,
tx.safeTxGas,
tx.baseGas,
tx.gasPrice,
tx.gasToken,
tx.refundReceiver,
from, from,
sigs, sigs,
) )
} else { } else {
transaction = await getApprovalTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from) transaction = await getApprovalTransaction(
safeInstance,
tx.recipient,
tx.value,
tx.data,
tx.operation,
tx.nonce,
tx.safeTxGas,
tx.baseGas,
tx.gasPrice,
tx.gasToken,
tx.refundReceiver,
from,
)
} }
const sendParams = { from, value: 0 } const sendParams = { from, value: 0 }
@ -111,12 +136,18 @@ const processTransaction = (
tx.recipient, tx.recipient,
tx.value, tx.value,
tx.data, tx.data,
CALL, tx.operation,
nonce, tx.nonce,
tx.safeTxGas,
tx.baseGas,
tx.gasPrice,
tx.gasToken,
tx.refundReceiver,
txHash, txHash,
from, from,
shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION, shouldExecute ? TX_TYPE_EXECUTION : TX_TYPE_CONFIRMATION,
) )
dispatch(fetchTransactions(safeAddress))
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }

View File

@ -8,12 +8,14 @@ export type ConfirmationProps = {
owner: Owner, owner: Owner,
type: TxServiceType, type: TxServiceType,
hash: string, hash: string,
signature?: string,
} }
export const makeConfirmation: RecordFactory<ConfirmationProps> = Record({ export const makeConfirmation: RecordFactory<ConfirmationProps> = Record({
owner: makeOwner(), owner: makeOwner(),
type: 'initialised', type: 'initialised',
hash: '', hash: '',
signature: null,
}) })
export type Confirmation = RecordOf<ConfirmationProps> export type Confirmation = RecordOf<ConfirmationProps>

View File

@ -2,16 +2,22 @@
import { List, Record } from 'immutable' import { List, Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable' import type { RecordFactory, RecordOf } from 'immutable'
import { type Confirmation } from '~/routes/safe/store/models/confirmation' import { type Confirmation } from '~/routes/safe/store/models/confirmation'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
export type TransactionStatus = 'awaiting_confirmations' | 'success' | 'cancelled' | 'awaiting_execution' export type TransactionStatus = 'awaiting_confirmations' | 'success' | 'cancelled' | 'awaiting_execution' | 'pending'
export type TransactionProps = { export type TransactionProps = {
name: string,
nonce: number, nonce: number,
value: string, value: string,
confirmations: List<Confirmation>, confirmations: List<Confirmation>,
recipient: string, recipient: string,
data: string, data?: string,
operation: number,
safeTxGas: number,
baseGas: number,
gasPrice: number,
gasToken: string,
refundReceiver: string,
isExecuted: boolean, isExecuted: boolean,
submissionDate: string, submissionDate: string,
executionDate: string, executionDate: string,
@ -26,15 +32,21 @@ export type TransactionProps = {
status?: TransactionStatus, status?: TransactionStatus,
isTokenTransfer: boolean, isTokenTransfer: boolean,
decodedParams?: Object, decodedParams?: Object,
refundParams?: Object,
} }
export const makeTransaction: RecordFactory<TransactionProps> = Record({ export const makeTransaction: RecordFactory<TransactionProps> = Record({
name: '',
nonce: 0, nonce: 0,
value: 0, value: 0,
confirmations: List([]), confirmations: List([]),
recipient: '', recipient: '',
data: '', data: null,
operation: 0,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: ZERO_ADDRESS,
refundReceiver: ZERO_ADDRESS,
isExecuted: false, isExecuted: false,
submissionDate: '', submissionDate: '',
executionDate: '', executionDate: '',
@ -49,6 +61,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
decimals: 18, decimals: 18,
isTokenTransfer: false, isTokenTransfer: false,
decodedParams: {}, decodedParams: {},
refundParams: null,
}) })
export type Transaction = RecordOf<TransactionProps> export type Transaction = RecordOf<TransactionProps>

View File

@ -285,6 +285,9 @@ export default createMuiTheme({
fontWeight: 'normal', fontWeight: 'normal',
paddingTop: xs, paddingTop: xs,
paddingBottom: xs, paddingBottom: xs,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}, },
}, },
MuiBackdrop: { MuiBackdrop: {

View File

@ -14901,10 +14901,10 @@ react-final-form-listeners@^1.0.2:
dependencies: dependencies:
"@babel/runtime" "^7.1.5" "@babel/runtime" "^7.1.5"
react-final-form@6.3.2: react-final-form@6.3.3:
version "6.3.2" version "6.3.3"
resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.3.2.tgz#2c331540c8f5cbf6fbe75ecd98849a03b34dba6e" resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.3.3.tgz#8856a92278de2733f83e88c75d9ef347294a66a0"
integrity sha512-3eXCd9ScouKbf7GJubhUP0s8+aYCsltjjWZtvOKV+E0AeRjXmzQZfUAsKM+395+v1dLIyenB3x22ZQms2tWFRQ== integrity sha512-hFLwLpLasywky46SovNEGlym7+N+3RKge1/cg+/fVec/YY0l4g0ihgjRof6PbkiYe4qGjKbwbrvlgfZ9/Uf8vw==
dependencies: dependencies:
"@babel/runtime" "^7.4.5" "@babel/runtime" "^7.4.5"
ts-essentials "^3.0.2" ts-essentials "^3.0.2"