mirror of
https://github.com/status-im/safe-react.git
synced 2025-01-22 23:59:37 +00:00
* Adds update safe modal * Implements update safe transaction * Moves the safeMasterCopyAddress and defaultFallbackHandlerAddress to a constant * Adds mainnet network Refactor logic * Removes the update button to non-owners * Updates safe-contracts package Removes hardcoded addresses * Refactor upgradeSafeToLastVersion Refactor getSafeVersion Refactor getEncodedMultiSendCallData Removes unused constants Adds tests for getEncodedMultiSendCallData and checkIfSafeNeedUpdate * Adds retro-compatibility with v1.0.0 * Improves test description * Fixs comments * Fix typo Removes spread * Exports proxy code to historicProxyCode.js * Updates yarn.lock
This commit is contained in:
parent
d51303f348
commit
2698dcb6e8
@ -33,8 +33,8 @@
|
|||||||
"precommit"
|
"precommit"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gnosis.pm/safe-contracts": "1.0.0",
|
"@gnosis.pm/safe-contracts": "1.1.1-dev.1",
|
||||||
"@gnosis.pm/util-contracts": "2.0.4",
|
"@gnosis.pm/util-contracts": "2.0.6",
|
||||||
"@material-ui/core": "4.8.0",
|
"@material-ui/core": "4.8.0",
|
||||||
"@material-ui/icons": "4.5.1",
|
"@material-ui/icons": "4.5.1",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.39",
|
"@material-ui/lab": "^4.0.0-alpha.39",
|
||||||
|
11
src/logic/contracts/historicProxyCode.js
Normal file
11
src/logic/contracts/historicProxyCode.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
// Code of the safe v1.0.0
|
||||||
|
const proxyCodeV10 =
|
||||||
|
'0x608060405273ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415603d573d6000fd5b3d6000f3fe'
|
||||||
|
// Old PayingProxyCode
|
||||||
|
const oldProxyCode =
|
||||||
|
'0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600'
|
||||||
|
|
||||||
|
export const isProxyCode = codeToCheck =>
|
||||||
|
codeToCheck === oldProxyCode || codeToCheck === proxyCodeV10
|
@ -1,15 +1,21 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import contract from 'truffle-contract'
|
import contract from 'truffle-contract'
|
||||||
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.json'
|
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxyFactory.json'
|
||||||
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/Proxy.json'
|
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import { simpleMemoize } from '~/components/forms/validator'
|
import { simpleMemoize } from '~/components/forms/validator'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
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 { isProxyCode } from '~/logic/contracts/historicProxyCode'
|
||||||
|
|
||||||
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
|
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
|
||||||
|
export const MULTI_SEND_ADDRESS = '0xB522a9f781924eD250A11C54105E51840B138AdD'
|
||||||
|
export const SAFE_MASTER_COPY_ADDRESS = '0x34CfAC646f301356fAa8B21e94227e3583Fe3F5F'
|
||||||
|
export const DEFAULT_FALLBACK_HANDLER_ADDRESS = '0xd5D82B6aDDc9027B22dCA772Aa68D5d74cdBdF44'
|
||||||
|
export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB05bDa35A'
|
||||||
|
|
||||||
|
|
||||||
let proxyFactoryMaster
|
let proxyFactoryMaster
|
||||||
let safeMaster
|
let safeMaster
|
||||||
@ -66,7 +72,7 @@ export const getSafeMasterContract = async () => {
|
|||||||
|
|
||||||
export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => {
|
export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => {
|
||||||
const gnosisSafeData = await safeMaster.contract.methods
|
const gnosisSafeData = await safeMaster.contract.methods
|
||||||
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
||||||
.encodeABI()
|
.encodeABI()
|
||||||
const proxyFactoryData = proxyFactoryMaster.contract.methods
|
const proxyFactoryData = proxyFactoryMaster.contract.methods
|
||||||
.createProxy(safeMaster.address, gnosisSafeData)
|
.createProxy(safeMaster.address, gnosisSafeData)
|
||||||
@ -88,7 +94,7 @@ export const estimateGasForDeployingSafe = async (
|
|||||||
userAccount: string,
|
userAccount: string,
|
||||||
) => {
|
) => {
|
||||||
const gnosisSafeData = await safeMaster.contract.methods
|
const gnosisSafeData = await safeMaster.contract.methods
|
||||||
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
||||||
.encodeABI()
|
.encodeABI()
|
||||||
const proxyFactoryData = proxyFactoryMaster.contract.methods
|
const proxyFactoryData = proxyFactoryMaster.contract.methods
|
||||||
.createProxy(safeMaster.address, gnosisSafeData)
|
.createProxy(safeMaster.address, gnosisSafeData)
|
||||||
@ -126,7 +132,49 @@ export const validateProxy = async (safeAddress: string): Promise<boolean> => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Old PayingProxyCode
|
|
||||||
const oldProxyCode = '0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634555d5c91461008b5780635c60da1b146100b6575b73ffffffffffffffffffffffffffffffffffffffff600054163660008037600080366000845af43d6000803e6000811415610086573d6000fd5b3d6000f35b34801561009757600080fd5b506100a061010d565b6040518082815260200191505060405180910390f35b3480156100c257600080fd5b506100cb610116565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60006002905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050905600'
|
|
||||||
return codeWithoutMetadata === oldProxyCode
|
return isProxyCode(codeWithoutMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MultiSendTransactionInstanceType = {
|
||||||
|
operation: number,
|
||||||
|
to: string,
|
||||||
|
value: number,
|
||||||
|
data: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEncodedMultiSendCallData = (txs: Array<MultiSendTransactionInstanceType>, web3: Object) => {
|
||||||
|
const multiSendAbi = [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
name: 'multiSend',
|
||||||
|
constant: false,
|
||||||
|
payable: false,
|
||||||
|
stateMutability: 'nonpayable',
|
||||||
|
inputs: [{ type: 'bytes', name: 'transactions' }],
|
||||||
|
outputs: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const multiSend = new web3.eth.Contract(multiSendAbi, MULTI_SEND_ADDRESS)
|
||||||
|
const encodeMultiSendCallData = multiSend.methods
|
||||||
|
.multiSend(
|
||||||
|
`0x${txs
|
||||||
|
.map((tx) => [
|
||||||
|
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
|
||||||
|
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),
|
||||||
|
web3.eth.abi.encodeParameter('uint256', tx.value).slice(-64),
|
||||||
|
web3.eth.abi
|
||||||
|
.encodeParameter(
|
||||||
|
'uint256',
|
||||||
|
web3.utils.hexToBytes(tx.data).length,
|
||||||
|
)
|
||||||
|
.slice(-64),
|
||||||
|
tx.data.replace(/^0x/, ''),
|
||||||
|
].join(''))
|
||||||
|
.join('')}`,
|
||||||
|
)
|
||||||
|
.encodeABI()
|
||||||
|
|
||||||
|
return encodeMultiSendCallData
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { type Operation } from '~/logic/safe/transactions'
|
|||||||
|
|
||||||
export const CALL = 0
|
export const CALL = 0
|
||||||
export const DELEGATE_CALL = 1
|
export const DELEGATE_CALL = 1
|
||||||
|
|
||||||
export const TX_TYPE_EXECUTION = 'execution'
|
export const TX_TYPE_EXECUTION = 'execution'
|
||||||
export const TX_TYPE_CONFIRMATION = 'confirmation'
|
export const TX_TYPE_CONFIRMATION = 'confirmation'
|
||||||
|
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import semverValid from 'semver/functions/valid'
|
import semverValid from 'semver/functions/valid'
|
||||||
import semverLessThan from 'semver/functions/lt'
|
import semverLessThan from 'semver/functions/lt'
|
||||||
import { getSafeMasterContract } from '~/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
import { getSafeLastVersion } from '~/config'
|
import { getSafeLastVersion } from '~/config'
|
||||||
|
|
||||||
export const getSafeVersion = async () => {
|
export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion) => {
|
||||||
let current
|
if (!gnosisSafeInstance || !lastSafeVersion) {
|
||||||
let latest
|
return null
|
||||||
try {
|
}
|
||||||
const safeMaster = await getSafeMasterContract()
|
const safeMasterVersion = await gnosisSafeInstance.VERSION()
|
||||||
const safeMasterVersion = await safeMaster.VERSION()
|
const current = semverValid(safeMasterVersion)
|
||||||
current = semverValid(safeMasterVersion)
|
const latest = semverValid(lastSafeVersion)
|
||||||
latest = semverValid(getSafeLastVersion())
|
const needUpdate = latest ? semverLessThan(current, latest) : false
|
||||||
const needUpdate = latest ? semverLessThan(current, latest) : false
|
return { current, latest, needUpdate }
|
||||||
|
}
|
||||||
|
|
||||||
return { current, latest, needUpdate }
|
export const getSafeVersion = async (safeAddress: string) => {
|
||||||
|
try {
|
||||||
|
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
return checkIfSafeNeedsUpdate(safeMaster, getSafeLastVersion())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
throw err
|
throw err
|
||||||
|
45
src/logic/safe/utils/updateSafe.js
Normal file
45
src/logic/safe/utils/updateSafe.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
DEFAULT_FALLBACK_HANDLER_ADDRESS,
|
||||||
|
getEncodedMultiSendCallData,
|
||||||
|
getGnosisSafeInstanceAt,
|
||||||
|
MULTI_SEND_ADDRESS, SAFE_MASTER_COPY_ADDRESS,
|
||||||
|
} from '~/logic/contracts/safeContracts'
|
||||||
|
import type { MultiSendTransactionInstanceType } from '~/logic/contracts/safeContracts'
|
||||||
|
import { DELEGATE_CALL } from '~/logic/safe/transactions'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
|
export const upgradeSafeToLastVersion = async (safeAddress: string, createTransaction: Function) => {
|
||||||
|
const sendTransactions = async (txs: Array<MultiSendTransactionInstanceType>) => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const encodeMultiSendCallData = getEncodedMultiSendCallData(txs, web3)
|
||||||
|
createTransaction({
|
||||||
|
safeAddress,
|
||||||
|
to: MULTI_SEND_ADDRESS,
|
||||||
|
valueInWei: 0,
|
||||||
|
txData: encodeMultiSendCallData,
|
||||||
|
notifiedTransaction: 'STANDARD_TX',
|
||||||
|
enqueueSnackbar: () => {},
|
||||||
|
closeSnackbar: () => {},
|
||||||
|
operation: DELEGATE_CALL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const fallbackHandlerTxData = safeInstance.contract.methods.setFallbackHandler(DEFAULT_FALLBACK_HANDLER_ADDRESS).encodeABI()
|
||||||
|
const updateSafeTxData = safeInstance.contract.methods.changeMasterCopy(SAFE_MASTER_COPY_ADDRESS).encodeABI()
|
||||||
|
const txs = [
|
||||||
|
{
|
||||||
|
operation: 0,
|
||||||
|
to: safeAddress,
|
||||||
|
value: 0,
|
||||||
|
data: fallbackHandlerTxData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operation: 0,
|
||||||
|
to: safeAddress,
|
||||||
|
value: 0,
|
||||||
|
data: updateSafeTxData,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return sendTransactions(txs)
|
||||||
|
}
|
@ -14,7 +14,7 @@ import Paragraph from '~/components/layout/Paragraph'
|
|||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
|
import { FIELD_LOAD_NAME, FIELD_LOAD_ADDRESS } from '~/routes/load/components/fields'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts'
|
import { getSafeMasterContract, SAFE_MASTER_COPY_ADDRESS_V10, validateProxy } from '~/logic/contracts/safeContracts'
|
||||||
import { secondary } from '~/theme/variables'
|
import { secondary } from '~/theme/variables'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -72,8 +72,7 @@ export const safeFieldsValidation = async (values: Object) => {
|
|||||||
)
|
)
|
||||||
const safeMaster = await getSafeMasterContract()
|
const safeMaster = await getSafeMasterContract()
|
||||||
const masterCopy = safeMaster.address
|
const masterCopy = safeMaster.address
|
||||||
|
const sameMasterCopy = checksummedProxyAddress === masterCopy || checksummedProxyAddress === SAFE_MASTER_COPY_ADDRESS_V10
|
||||||
const sameMasterCopy = checksummedProxyAddress === masterCopy
|
|
||||||
if (!sameMasterCopy) {
|
if (!sameMasterCopy) {
|
||||||
errors[FIELD_LOAD_ADDRESS] = SAFE_MASTERCOPY_ERROR
|
errors[FIELD_LOAD_ADDRESS] = SAFE_MASTERCOPY_ERROR
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export const calculateValuesAfterRemoving = (
|
|||||||
const initialValues = { ...values }
|
const initialValues = { ...values }
|
||||||
|
|
||||||
const numOwnersAfterRemoving = notRemovedOwners - 1
|
const numOwnersAfterRemoving = notRemovedOwners - 1
|
||||||
// muevo indices
|
|
||||||
for (let i = index; i < numOwnersAfterRemoving; i += 1) {
|
for (let i = index; i < numOwnersAfterRemoving; i += 1) {
|
||||||
initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)]
|
initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)]
|
||||||
initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)]
|
initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)]
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { withSnackbar } from 'notistack'
|
import { withSnackbar } from 'notistack'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import Block from '~/components/layout/Block'
|
import Block from '~/components/layout/Block'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Field from '~/components/forms/Field'
|
import Field from '~/components/forms/Field'
|
||||||
@ -16,15 +17,20 @@ import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
|||||||
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||||
|
import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import { grantedSelector } from '~/routes/safe/container/selector'
|
||||||
|
|
||||||
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
|
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
|
||||||
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
||||||
|
export const SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID = 'update-safe-name-btn'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
updateSafe: Function,
|
updateSafe: Function,
|
||||||
enqueueSnackbar: Function,
|
enqueueSnackbar: Function,
|
||||||
|
createTransaction: Function,
|
||||||
closeSnackbar: Function,
|
closeSnackbar: Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,10 +39,17 @@ const useStyles = makeStyles(styles)
|
|||||||
const SafeDetails = (props: Props) => {
|
const SafeDetails = (props: Props) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [safeVersions, setSafeVersions] = React.useState({ current: null, latest: null, needUpdate: false })
|
const [safeVersions, setSafeVersions] = React.useState({ current: null, latest: null, needUpdate: false })
|
||||||
|
const isUserOwner = useSelector(grantedSelector)
|
||||||
const {
|
const {
|
||||||
safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar,
|
safeAddress, safeName, updateSafe, enqueueSnackbar, closeSnackbar, createTransaction,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const [isModalOpen, setModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const toggleModal = () => {
|
||||||
|
setModalOpen((prevOpen) => !prevOpen)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = (values) => {
|
const handleSubmit = (values) => {
|
||||||
updateSafe({ address: safeAddress, name: values.safeName })
|
updateSafe({ address: safeAddress, name: values.safeName })
|
||||||
|
|
||||||
@ -44,10 +57,14 @@ const SafeDetails = (props: Props) => {
|
|||||||
showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
showSnackbar(notification.afterExecution.noMoreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpdateSafe = () => {
|
||||||
|
setModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getVersion = async () => {
|
const getVersion = async () => {
|
||||||
try {
|
try {
|
||||||
const { current, latest, needUpdate } = await getSafeVersion()
|
const { current, latest, needUpdate } = await getSafeVersion(safeAddress)
|
||||||
setSafeVersions({ current, latest, needUpdate })
|
setSafeVersions({ current, latest, needUpdate })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSafeVersions({ current: 'Version not defined' })
|
setSafeVersions({ current: 'Version not defined' })
|
||||||
@ -70,6 +87,22 @@ const SafeDetails = (props: Props) => {
|
|||||||
{safeVersions.needUpdate && ` (there's a newer version: ${safeVersions.latest})`}
|
{safeVersions.needUpdate && ` (there's a newer version: ${safeVersions.latest})`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
|
{safeVersions.needUpdate && isUserOwner ? (
|
||||||
|
<Row align="end" grow>
|
||||||
|
<Paragraph>
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdateSafe}
|
||||||
|
className={classes.saveBtn}
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
testId={SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID}
|
||||||
|
>
|
||||||
|
Update Safe
|
||||||
|
</Button>
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
) : null}
|
||||||
</Block>
|
</Block>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
<Heading tag="h2">Modify Safe name</Heading>
|
<Heading tag="h2">Modify Safe name</Heading>
|
||||||
@ -104,6 +137,14 @@ const SafeDetails = (props: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Modal
|
||||||
|
title="Update Safe"
|
||||||
|
description="Update Safe"
|
||||||
|
handleClose={toggleModal}
|
||||||
|
open={isModalOpen}
|
||||||
|
>
|
||||||
|
<UpdateSafeModal onClose={toggleModal} safeAddress={safeAddress} createTransaction={createTransaction} />
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</GnoForm>
|
</GnoForm>
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import { withStyles } from '@material-ui/styles'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import GnoForm from '~/components/forms/GnoForm'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import { styles } from './style'
|
||||||
|
import { upgradeSafeToLastVersion } from '~/logic/safe/utils/updateSafe'
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function,
|
||||||
|
createTransaction: Function,
|
||||||
|
classes: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const UpdateSafeModal = ({
|
||||||
|
onClose, classes, safeAddress, createTransaction,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// Call the update safe method
|
||||||
|
await upgradeSafeToLastVersion(safeAddress, createTransaction)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph className={classes.headingText} weight="bolder" noMargin>
|
||||||
|
Change required confirmations
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.close} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm onSubmit={handleSubmit}>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<Block className={classes.modalContent}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>
|
||||||
|
Update now to take advantage of new features and the highest security standards available.
|
||||||
|
</Paragraph>
|
||||||
|
<Block>
|
||||||
|
This update includes:
|
||||||
|
<ul>
|
||||||
|
<li>Compatibility with new asset types (ERC-721 / ERC-1155)</li>
|
||||||
|
<li>Improved interoperability with modules</li>
|
||||||
|
<li>Minor security improvements</li>
|
||||||
|
</ul>
|
||||||
|
</Block>
|
||||||
|
<Paragraph>
|
||||||
|
You will need to confirm this update just like any other transaction. This means other owners will have to confirm the update in case more than one confirmation is required for this Safe.
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button minWidth={140} onClick={onClose}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" color="primary" minWidth={140} variant="contained">
|
||||||
|
Update Safe
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</GnoForm>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(UpdateSafeModal)
|
46
src/routes/safe/components/Settings/UpdateSafeModal/style.js
Normal file
46
src/routes/safe/components/Settings/UpdateSafeModal/style.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
lg, md, sm, secondaryText,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: secondaryText,
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
headingText: {
|
||||||
|
fontSize: '20px',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
ownersText: {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
inputRow: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-25px',
|
||||||
|
},
|
||||||
|
})
|
@ -170,11 +170,7 @@ class Settings extends React.Component<Props, State> {
|
|||||||
<Col className={classes.contents} layout="column">
|
<Col className={classes.contents} layout="column">
|
||||||
<Block className={classes.container}>
|
<Block className={classes.container}>
|
||||||
{menuOptionIndex === 1 && (
|
{menuOptionIndex === 1 && (
|
||||||
<SafeDetails
|
<SafeDetails safeAddress={safeAddress} safeName={safeName} updateSafe={updateSafe} createTransaction={createTransaction} />
|
||||||
safeAddress={safeAddress}
|
|
||||||
safeName={safeName}
|
|
||||||
updateSafe={updateSafe}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{menuOptionIndex === 2 && (
|
{menuOptionIndex === 2 && (
|
||||||
<ManageOwners
|
<ManageOwners
|
||||||
|
@ -116,35 +116,13 @@ const createTransaction = ({
|
|||||||
try {
|
try {
|
||||||
if (isExecution) {
|
if (isExecution) {
|
||||||
tx = await getExecutionTransaction(
|
tx = await getExecutionTransaction(
|
||||||
safeInstance,
|
safeInstance, to, valueInWei, txData, operation, nonce,
|
||||||
to,
|
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs,
|
||||||
valueInWei,
|
|
||||||
txData,
|
|
||||||
operation,
|
|
||||||
nonce,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
ZERO_ADDRESS,
|
|
||||||
ZERO_ADDRESS,
|
|
||||||
from,
|
|
||||||
sigs
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
tx = await getApprovalTransaction(
|
tx = await getApprovalTransaction(
|
||||||
safeInstance,
|
safeInstance, to, valueInWei, txData, operation, nonce,
|
||||||
to,
|
0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, from, sigs,
|
||||||
valueInWei,
|
|
||||||
txData,
|
|
||||||
operation,
|
|
||||||
nonce,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
ZERO_ADDRESS,
|
|
||||||
ZERO_ADDRESS,
|
|
||||||
from,
|
|
||||||
sigs
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,8 +118,9 @@ const notificationsMiddleware = (store: Store<GlobalState>) => (
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ADD_SAFE: {
|
case ADD_SAFE: {
|
||||||
const { needUpdate } = await getSafeVersion()
|
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
|
const { needUpdate } = await getSafeVersion(safe.address)
|
||||||
|
|
||||||
const notificationKey = `${safe.address}`
|
const notificationKey = `${safe.address}`
|
||||||
if (needUpdate) {
|
if (needUpdate) {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
37
src/test/safe.dom.upgrade.test.js
Normal file
37
src/test/safe.dom.upgrade.test.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// @flow
|
||||||
|
import Web3 from 'web3'
|
||||||
|
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
|
import {
|
||||||
|
DEFAULT_FALLBACK_HANDLER_ADDRESS,
|
||||||
|
getEncodedMultiSendCallData,
|
||||||
|
SAFE_MASTER_COPY_ADDRESS,
|
||||||
|
} from '~/logic/contracts/safeContracts'
|
||||||
|
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
|
|
||||||
|
describe('Upgrade a Safe', () => {
|
||||||
|
it('Calls getEncodedMultiSendCallData with a list of MultiSendTransactionInstanceType and returns the multiSend data encoded', async () => {
|
||||||
|
const safeAddress = ZERO_ADDRESS
|
||||||
|
const web3 = new Web3(new Web3.providers.HttpProvider(''))
|
||||||
|
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
|
||||||
|
const fallbackHandlerTxData = safeInstance.methods.setFallbackHandler(DEFAULT_FALLBACK_HANDLER_ADDRESS).encodeABI()
|
||||||
|
const updateSafeTxData = safeInstance.methods.changeMasterCopy(SAFE_MASTER_COPY_ADDRESS).encodeABI()
|
||||||
|
const txs = [
|
||||||
|
{
|
||||||
|
operation: 0,
|
||||||
|
to: safeAddress,
|
||||||
|
value: 0,
|
||||||
|
data: fallbackHandlerTxData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operation: 0,
|
||||||
|
to: safeAddress,
|
||||||
|
value: 0,
|
||||||
|
data: updateSafeTxData,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const expectedEncodedData = '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000f200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f08a0323000000000000000000000000d5d82b6addc9027b22dca772aa68d5d74cdbdf44000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247de7edef00000000000000000000000034cfac646f301356faa8b21e94227e3583fe3f5f0000000000000000000000000000'
|
||||||
|
const multiSendTxData = getEncodedMultiSendCallData(txs, web3)
|
||||||
|
expect(multiSendTxData).toEqual(expectedEncodedData)
|
||||||
|
})
|
||||||
|
})
|
32
src/test/safe.dom.version.test.js
Normal file
32
src/test/safe.dom.version.test.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// @flow
|
||||||
|
import Web3 from 'web3'
|
||||||
|
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
|
import { checkIfSafeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
|
||||||
|
|
||||||
|
|
||||||
|
describe('Check safe version', () => {
|
||||||
|
it('Calls checkIfSafeNeedUpdate, should return true if the safe version is bellow the target one', async () => {
|
||||||
|
const web3 = new Web3(new Web3.providers.HttpProvider(''))
|
||||||
|
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
|
||||||
|
safeInstance.VERSION = () => '1.0.0'
|
||||||
|
const targetVersion = '1.1.1'
|
||||||
|
const { needUpdate } = await checkIfSafeNeedsUpdate(safeInstance, targetVersion)
|
||||||
|
expect(needUpdate).toEqual(true)
|
||||||
|
})
|
||||||
|
it('Calls checkIfSafeNeedUpdate, should return false if the safe version is over the target one', async () => {
|
||||||
|
const web3 = new Web3(new Web3.providers.HttpProvider(''))
|
||||||
|
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
|
||||||
|
safeInstance.VERSION = () => '2.0.0'
|
||||||
|
const targetVersion = '1.1.1'
|
||||||
|
const { needUpdate } = await checkIfSafeNeedsUpdate(safeInstance, targetVersion)
|
||||||
|
expect(needUpdate).toEqual(false)
|
||||||
|
})
|
||||||
|
it('Calls checkIfSafeNeedUpdate, should return false if the safe version is equal the target one', async () => {
|
||||||
|
const web3 = new Web3(new Web3.providers.HttpProvider(''))
|
||||||
|
const safeInstance = new web3.eth.Contract(GnosisSafeSol.abi)
|
||||||
|
safeInstance.VERSION = () => '1.1.1'
|
||||||
|
const targetVersion = '1.1.1'
|
||||||
|
const { needUpdate } = await checkIfSafeNeedsUpdate(safeInstance, targetVersion)
|
||||||
|
expect(needUpdate).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user