V1.6.1 - Bugfixes (#432)

* Change label for cancelled tx to 'cancelled' (#393)

* Feature 309: Listen for web3connect disconnect event (#324)

* add 'disconnect' event listener for web3connect

* Update web3connect, set preventDuplicate to true for disconnected message

Co-authored-by: Germán Martínez <germartinez@users.noreply.github.com>

* Update package.json

* Fix signatures order in txData (#429)

* Fix signatures order in tx data

* extract generateSignaturesFromTxConfirmations to a separate file, rewrite test for signatures

Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>

* check if txNonce is defined in createTransaction, not for truthy

* fix txNonce check in createTransaction

* check if transaction.nonce is defined, not for thruthy value

* fix check for defined nonce

Co-authored-by: Germán Martínez <germartinez@users.noreply.github.com>
This commit is contained in:
Mikhail Mikheev 2020-01-14 19:48:31 +04:00 committed by GitHub
parent 1ddd17423e
commit d391d6e7b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 496 additions and 1061 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "safe-react", "name": "safe-react",
"version": "1.6.0", "version": "1.6.1",
"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": {
@ -76,7 +76,7 @@
"semver": "^7.1.1", "semver": "^7.1.1",
"squarelink": "^1.1.4", "squarelink": "^1.1.4",
"web3": "1.2.4", "web3": "1.2.4",
"web3connect": "^1.0.0-beta.23" "web3connect": "^1.0.0-beta.25"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.7.5", "@babel/cli": "7.7.5",

View File

@ -7,7 +7,7 @@ import Fortmatic from 'fortmatic'
import Portis from '@portis/web3' import Portis from '@portis/web3'
import Squarelink from 'squarelink' import Squarelink from 'squarelink'
import Button from '~/components/layout/Button' import Button from '~/components/layout/Button'
import { fetchProvider } from '~/logic/wallets/store/actions' import { fetchProvider, removeProvider } from '~/logic/wallets/store/actions'
import { getNetwork } from '~/config' import { getNetwork } from '~/config'
import { store } from '~/store' import { store } from '~/store'
@ -62,6 +62,10 @@ web3Connect.on('connect', (provider: any) => {
} }
}) })
web3Connect.on('disconnect', () => {
store.dispatch(removeProvider())
})
type Props = { type Props = {
enqueueSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function, closeSnackbar: Function,

View File

@ -89,6 +89,7 @@ export const NOTIFICATIONS: Notifications = {
variant: SUCCESS, variant: SUCCESS,
persist: false, persist: false,
autoHideDuration: shortDuration, autoHideDuration: shortDuration,
preventDuplicate: true,
}, },
}, },
UNLOCK_WALLET_MSG: { UNLOCK_WALLET_MSG: {

View File

@ -1,19 +1,38 @@
// @flow // @flow
import { List } from 'immutable' import { List } from 'immutable'
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
const generateSignatureFrom = (account: string) => `000000000000000000000000${account.replace( // https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
'0x', // https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
'', export const generateSignaturesFromTxConfirmations = (
)}000000000000000000000000000000000000000000000000000000000000000001` confirmations: List<Confirmation>,
preApprovingOwner?: string,
) => {
// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed).
const confirmationsMap = confirmations.reduce((map, obj) => {
map[obj.owner.address.toLowerCase()] = obj // eslint-disable-line no-param-reassign
return map
}, {})
export const buildSignaturesFrom = (ownersWhoHasSigned: List<string>, sender: string) => { if (preApprovingOwner) {
const signatures = ownersWhoHasSigned.push(sender) confirmationsMap[preApprovingOwner.toLowerCase()] = { owner: preApprovingOwner }
const orderedSignatures = signatures.sort() // JS by default sorts in a non case-senstive way }
let sigs = '0x' let sigs = '0x'
orderedSignatures.forEach((owner: string) => { Object.keys(confirmationsMap)
sigs += generateSignatureFrom(owner) .sort()
}) .forEach((addr) => {
const conf = confirmationsMap[addr]
if (conf.signature) {
sigs += conf.signature.slice(2)
} else {
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
sigs += `000000000000000000000000${addr.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
})
return sigs return sigs
} }

View File

@ -2,7 +2,7 @@
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
import { type Transaction } from '~/routes/safe/store/models/transaction' import { type Transaction } from '~/routes/safe/store/models/transaction'
import { getWeb3, getAccountFrom } from '~/logic/wallets/getWeb3' import { getWeb3, getAccountFrom } from '~/logic/wallets/getWeb3'
import { generateSignaturesFromTxConfirmations } from '~/routes/safe/store/actions/processTransaction' import { generateSignaturesFromTxConfirmations } from '~/logic/safe/safeTxSigner'
import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
import { CALL } from '.' import { CALL } from '.'

View File

@ -1,15 +1,16 @@
// @flow // @flow
import { createAction } from 'redux-actions' import { createAction } from 'redux-actions'
import type { Dispatch as ReduxDispatch } from 'redux' import type { Dispatch as ReduxDispatch } from 'redux'
import { NOTIFICATIONS, showSnackbar } from '~/logic/notifications' import { NOTIFICATIONS, enhanceSnackbarForAction } from '~/logic/notifications'
import { getWeb3, resetWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3, resetWeb3 } from '~/logic/wallets/getWeb3'
import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar'
export const REMOVE_PROVIDER = 'REMOVE_PROVIDER' export const REMOVE_PROVIDER = 'REMOVE_PROVIDER'
const removeProvider = createAction<string, *, *>(REMOVE_PROVIDER) const removeProvider = createAction<string, *, *>(REMOVE_PROVIDER)
export default (enqueueSnackbar: Function, closeSnackbar: Function) => (dispatch: ReduxDispatch<*>) => { export default () => (dispatch: ReduxDispatch<*>) => {
showSnackbar(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, enqueueSnackbar, closeSnackbar) dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.WALLET_DISCONNECTED_MSG)))
const web3 = getWeb3() const web3 = getWeb3()

View File

@ -27,7 +27,7 @@ const statusToIcon = {
const statusToLabel = { const statusToLabel = {
success: 'Success', success: 'Success',
cancelled: 'Failed', cancelled: 'Cancelled',
awaiting_your_confirmation: 'Awaiting your confirmation', awaiting_your_confirmation: 'Awaiting your confirmation',
awaiting_confirmations: 'Awaiting confirmations', awaiting_confirmations: 'Awaiting confirmations',
awaiting_execution: 'Awaiting execution', awaiting_execution: 'Awaiting execution',

View File

@ -1,6 +1,10 @@
// @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 { import {
safeSelector, safeSelector,
safeActiveTokensSelector, safeActiveTokensSelector,
@ -11,16 +15,29 @@ import {
type RouterProps, type RouterProps,
type SafeSelectorProps, type SafeSelectorProps,
} from '~/routes/safe/store/selectors' } from '~/routes/safe/store/selectors'
import { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors' import {
providerNameSelector,
userAccountSelector,
networkSelector,
} from '~/logic/wallets/store/selectors'
import { type Safe } from '~/routes/safe/store/models/safe' import { type Safe } from '~/routes/safe/store/models/safe'
import { type GlobalState } from '~/store' import { type GlobalState } from '~/store'
import { isUserOwner } from '~/logic/wallets/ethAddresses' import { isUserOwner } from '~/logic/wallets/ethAddresses'
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors' import {
orderedTokenListSelector,
tokensSelector,
} from '~/logic/tokens/store/selectors'
import { type Token } from '~/logic/tokens/store/model/token' import { type Token } from '~/logic/tokens/store/model/token'
import { type Transaction, type TransactionStatus } from '~/routes/safe/store/models/transaction' import {
type Transaction,
type TransactionStatus,
} from '~/routes/safe/store/models/transaction'
import { safeParamAddressSelector } from '../store/selectors' import { safeParamAddressSelector } from '../store/selectors'
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers' import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
import { currencyValuesListSelector, currentCurrencySelector } from '~/logic/currencyValues/store/selectors' import {
currencyValuesListSelector,
currentCurrencySelector,
} from '~/logic/currencyValues/store/selectors'
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues' import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction' import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
@ -35,10 +52,14 @@ export type SelectorProps = {
safeUrl: string, safeUrl: string,
currencySelected: string, currencySelected: string,
currencyValues: BalanceCurrencyType[], currencyValues: BalanceCurrencyType[],
transactions: List<Transaction | IncomingTransaction>, transactions: List<Transaction | IncomingTransaction>
} }
const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): TransactionStatus => { const getTxStatus = (
tx: Transaction,
userAddress: string,
safe: Safe,
): TransactionStatus => {
let txStatus let txStatus
if (tx.executionTxHash) { if (tx.executionTxHash) {
txStatus = 'success' txStatus = 'success'
@ -51,37 +72,54 @@ const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): Transact
} else if (!tx.confirmations.size) { } else if (!tx.confirmations.size) {
txStatus = 'pending' txStatus = 'pending'
} else { } else {
const userConfirmed = tx.confirmations.filter((conf) => conf.owner.address === userAddress).size === 1 const userConfirmed = tx.confirmations.filter((conf) => conf.owner.address === userAddress)
.size === 1
const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1 const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations' txStatus = !userConfirmed && userIsSafeOwner
? 'awaiting_your_confirmation'
: 'awaiting_confirmations'
} }
return txStatus return txStatus
} }
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector( export const grantedSelector: Selector<
GlobalState,
RouterProps,
boolean
> = createSelector(
userAccountSelector, userAccountSelector,
safeSelector, safeSelector,
(userAccount: string, safe: Safe | typeof undefined): boolean => isUserOwner(safe, userAccount), (userAccount: string, safe: Safe | typeof undefined): boolean => isUserOwner(safe, userAccount),
) )
const safeEthAsTokenSelector: Selector<GlobalState, RouterProps, ?Token> = createSelector( const safeEthAsTokenSelector: Selector<
safeSelector, GlobalState,
(safe: Safe) => { RouterProps,
if (!safe) { ?Token
return undefined > = createSelector(safeSelector, (safe: Safe) => {
} if (!safe) {
return undefined
}
return getEthAsToken(safe.ethBalance) return getEthAsToken(safe.ethBalance)
}, })
)
const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List<Token>> = createSelector( const extendedSafeTokensSelector: Selector<
GlobalState,
RouterProps,
List<Token>
> = createSelector(
safeActiveTokensSelector, safeActiveTokensSelector,
safeBalancesSelector, safeBalancesSelector,
tokensSelector, tokensSelector,
safeEthAsTokenSelector, safeEthAsTokenSelector,
(safeTokens: List<string>, balances: Map<string, string>, tokensList: Map<string, Token>, ethAsToken: Token) => { (
safeTokens: List<string>,
balances: Map<string, string>,
tokensList: Map<string, Token>,
ethAsToken: Token,
) => {
const extendedTokens = Map().withMutations((map) => { const extendedTokens = Map().withMutations((map) => {
safeTokens.forEach((tokenAddress: string) => { safeTokens.forEach((tokenAddress: string) => {
const baseToken = tokensList.get(tokenAddress) const baseToken = tokensList.get(tokenAddress)
@ -101,7 +139,11 @@ const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List<Token>
}, },
) )
const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction | IncomingTransaction>> = createSelector( const extendedTransactionsSelector: Selector<
GlobalState,
RouterProps,
List<Transaction | IncomingTransaction>
> = createSelector(
safeSelector, safeSelector,
userAccountSelector, userAccountSelector,
safeTransactionsSelector, safeTransactionsSelector,
@ -114,17 +156,21 @@ const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Tran
// it means that the transaction was cancelled (Replaced) and shouldn't get executed // it means that the transaction was cancelled (Replaced) and shouldn't get executed
let replacementTransaction let replacementTransaction
if (!tx.isExecuted) { if (!tx.isExecuted) {
replacementTransaction = transactions.size > 1 && transactions.findLast( replacementTransaction = transactions.size > 1
(transaction) => ( && transactions.findLast(
transaction.isExecuted && transaction.nonce && transaction.nonce >= tx.nonce (transaction) => transaction.isExecuted
), && transaction.nonce != null
) && transaction.nonce >= tx.nonce,
)
if (replacementTransaction) { if (replacementTransaction) {
extendedTx = tx.set('cancelled', true) extendedTx = tx.set('cancelled', true)
} }
} }
return extendedTx.set('status', getTxStatus(extendedTx, userAddress, safe)) return extendedTx.set(
'status',
getTxStatus(extendedTx, userAddress, safe),
)
}) })
return List([...extendedTransactions, ...incomingTransactions]) return List([...extendedTransactions, ...incomingTransactions])

View File

@ -78,7 +78,7 @@ const createTransaction = ({
const from = userAccountSelector(state) const from = userAccountSelector(state)
const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const threshold = await safeInstance.getThreshold() const threshold = await safeInstance.getThreshold()
const nonce = txNonce || await getLastPendingTxNonce(safeAddress) const nonce = typeof txNonce !== 'undefined' ? 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

View File

@ -1,7 +1,5 @@
// @flow // @flow
import type { Dispatch as ReduxDispatch } 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 { 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 fetchSafe from '~/routes/safe/store/actions/fetchSafe'
@ -16,44 +14,10 @@ import {
TX_TYPE_EXECUTION, TX_TYPE_EXECUTION,
TX_TYPE_CONFIRMATION, TX_TYPE_CONFIRMATION,
} from '~/logic/safe/transactions' } from '~/logic/safe/transactions'
import { generateSignaturesFromTxConfirmations } from '~/logic/safe/safeTxSigner'
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'
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
export const generateSignaturesFromTxConfirmations = (
confirmations: List<Confirmation>,
preApprovingOwner?: string,
) => {
// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed).
const confirmationsMap = confirmations.reduce((map, obj) => {
map[obj.owner.address] = obj // eslint-disable-line no-param-reassign
return map
}, {})
if (preApprovingOwner) {
confirmationsMap[preApprovingOwner] = { owner: preApprovingOwner }
}
let sigs = '0x'
Object.keys(confirmationsMap)
.sort()
.forEach((addr) => {
const conf = confirmationsMap[addr]
if (conf.signature) {
sigs += conf.signature.slice(2)
} else {
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
sigs += `000000000000000000000000${addr.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
})
return sigs
}
type ProcessTransactionArgs = { type ProcessTransactionArgs = {
safeAddress: string, safeAddress: string,
tx: Transaction, tx: Transaction,

View File

@ -1,6 +1,8 @@
// @flow // @flow
import { List } from 'immutable' import { List } from 'immutable'
import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner' import { generateSignaturesFromTxConfirmations } from '~/logic/safe/safeTxSigner'
const makeMockConfirmation = (address: string) => ({ owner: { address } })
describe('Signatures Blockchain Test', () => { describe('Signatures Blockchain Test', () => {
it('generates signatures in natural order even checksumed', async () => { it('generates signatures in natural order even checksumed', async () => {
@ -8,39 +10,104 @@ describe('Signatures Blockchain Test', () => {
const userA = 'baR' const userA = 'baR'
const userB = 'baz' const userB = 'baz'
const userC = 'foZa' const userC = 'foZa'
const sender = 'foZ' const userD = 'foZ'
const confirmationA = makeMockConfirmation(userA)
const confirmationB = makeMockConfirmation(userB)
const confirmationC = makeMockConfirmation(userC)
const confirmationD = makeMockConfirmation(userD)
// WHEN // WHEN
const result = '0x' const result = '0x'
// eslint-disable-next-line // eslint-disable-next-line
+ '000000000000000000000000' + + "000000000000000000000000" +
'baR' 'bar'
+ '000000000000000000000000000000000000000000000000000000000000000001' + '000000000000000000000000000000000000000000000000000000000000000001'
// eslint-disable-next-line // eslint-disable-next-line
+ '000000000000000000000000' + + "000000000000000000000000" +
'baz' 'baz'
+ '000000000000000000000000000000000000000000000000000000000000000001' + '000000000000000000000000000000000000000000000000000000000000000001'
// eslint-disable-next-line // eslint-disable-next-line
+ '000000000000000000000000' + + "000000000000000000000000" +
'foZ' 'foz'
+ '000000000000000000000000000000000000000000000000000000000000000001' + '000000000000000000000000000000000000000000000000000000000000000001'
// eslint-disable-next-line // eslint-disable-next-line
+ '000000000000000000000000' + + "000000000000000000000000" +
'foZa' 'foza'
+ '000000000000000000000000000000000000000000000000000000000000000001' + '000000000000000000000000000000000000000000000000000000000000000001'
// THEN // THEN
expect(buildSignaturesFrom(List([userA, userB, userC]), sender)).toEqual(result) expect(
expect(buildSignaturesFrom(List([userB, userA, userC]), sender)).toEqual(result) generateSignaturesFromTxConfirmations(
expect(buildSignaturesFrom(List([userA, sender, userC]), userB)).toEqual(result) List([confirmationA, confirmationB, confirmationC]),
expect(buildSignaturesFrom(List([sender, userA, userC]), userB)).toEqual(result) userD,
expect(buildSignaturesFrom(List([userB, sender, userC]), userA)).toEqual(result) ),
expect(buildSignaturesFrom(List([sender, userB, userC]), userA)).toEqual(result) ).toEqual(result)
expect(buildSignaturesFrom(List([userA, userB, sender]), userC)).toEqual(result) expect(
expect(buildSignaturesFrom(List([userB, userA, sender]), userC)).toEqual(result) generateSignaturesFromTxConfirmations(
expect(buildSignaturesFrom(List([userA, sender, userB]), userC)).toEqual(result) List([confirmationB, confirmationA, confirmationC]),
expect(buildSignaturesFrom(List([sender, userA, userB]), userC)).toEqual(result) userD,
expect(buildSignaturesFrom(List([userB, sender, userA]), userC)).toEqual(result) ),
expect(buildSignaturesFrom(List([sender, userB, userA]), userC)).toEqual(result) ).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationA, confirmationD, confirmationC]),
userB,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationD, confirmationA, confirmationC]),
userB,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationB, confirmationD, confirmationC]),
userA,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationD, confirmationB, confirmationC]),
userA,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationA, confirmationB, confirmationD]),
userC,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationB, confirmationA, confirmationD]),
userC,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationA, confirmationD, confirmationB]),
userC,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationD, confirmationA, confirmationB]),
userC,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationB, confirmationD, confirmationA]),
userC,
),
).toEqual(result)
expect(
generateSignaturesFromTxConfirmations(
List([confirmationD, confirmationB, confirmationA]),
userC,
),
).toEqual(result)
}) })
}) })

1243
yarn.lock

File diff suppressed because it is too large Load Diff