[AddressBook v2] - Avoid using checksumAddress inside reducer (#2355)

This commit is contained in:
Fernando 2021-06-01 09:13:18 -03:00 committed by GitHub
parent e58b6d6b7b
commit fc0c450a74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 192 additions and 193 deletions

View File

@ -4,6 +4,7 @@ import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { isFeatureEnabled } from 'src/config' import { isFeatureEnabled } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d' import { FEATURES } from 'src/config/networks/network.d'
import { isValidAddress } from 'src/utils/isValidAddress'
type ValidatorReturnType = string | undefined type ValidatorReturnType = string | undefined
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
@ -73,9 +74,7 @@ export const mustBeHexData = (data: string): ValidatorReturnType => {
export const mustBeAddressHash = memoize( export const mustBeAddressHash = memoize(
(address: string): ValidatorReturnType => { (address: string): ValidatorReturnType => {
const errorMessage = 'Must be a valid address' const errorMessage = 'Must be a valid address'
const startsWith0x = address?.startsWith('0x') return isValidAddress(address) ? undefined : errorMessage
const isAddress = getWeb3().utils.isAddress(address)
return startsWith0x && isAddress ? undefined : errorMessage
}, },
) )

View File

@ -4,7 +4,6 @@ import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/
import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions' import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions'
import { getEntryIndex, isValidAddressBookName } from 'src/logic/addressBook/utils' import { getEntryIndex, isValidAddressBookName } from 'src/logic/addressBook/utils'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
@ -14,16 +13,13 @@ export default handleActions<AppReduxState['addressBook'], Payloads>(
{ {
[ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE]: (state, action: Action<AddressBookEntry>) => { [ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE]: (state, action: Action<AddressBookEntry>) => {
const newState = [...state] const newState = [...state]
const { address, ...rest } = action.payload const addressBookEntry = action.payload
if (!isValidAddressBookName(rest.name)) { if (!isValidAddressBookName(addressBookEntry.name)) {
// prevent adding an invalid name // prevent adding an invalid name
return newState return newState
} }
// always checksum the address before storing it
const addressBookEntry = { address: checksumAddress(address), ...rest }
const entryIndex = getEntryIndex(newState, addressBookEntry) const entryIndex = getEntryIndex(newState, addressBookEntry)
// update // update
@ -56,18 +52,14 @@ export default handleActions<AppReduxState['addressBook'], Payloads>(
// exclude those entries with invalid name // exclude those entries with invalid name
.filter(({ name }) => isValidAddressBookName(name)) .filter(({ name }) => isValidAddressBookName(name))
.forEach((addressBookEntry) => { .forEach((addressBookEntry) => {
const { address, ...rest } = addressBookEntry const entryIndex = getEntryIndex(newState, addressBookEntry)
// always checksum the address before storing it
const newAddressBookEntry = { address: checksumAddress(address), ...rest }
const entryIndex = getEntryIndex(newState, newAddressBookEntry)
if (entryIndex >= 0) { if (entryIndex >= 0) {
// update // update
newState[entryIndex] = newAddressBookEntry newState[entryIndex] = addressBookEntry
} else { } else {
// add // add
newState.push(newAddressBookEntry) newState.push(addressBookEntry)
} }
}) })

View File

@ -15,6 +15,7 @@ import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe'
import { history } from 'src/store' import { history } from 'src/store'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
import { isValidAddress } from 'src/utils/isValidAddress'
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
@ -59,7 +60,7 @@ const Load = (): ReactElement => {
const onLoadSafeSubmit = async (values: LoadFormValues) => { const onLoadSafeSubmit = async (values: LoadFormValues) => {
let safeAddress = values[FIELD_LOAD_ADDRESS] let safeAddress = values[FIELD_LOAD_ADDRESS]
if (!safeAddress) { if (!isValidAddress(safeAddress)) {
console.error('failed to add Safe address', JSON.stringify(values)) console.error('failed to add Safe address', JSON.stringify(values))
return return
} }

View File

@ -164,7 +164,7 @@ const Open = (): ReactElement => {
setShowProgress(true) setShowProgress(true)
} }
const onSafeCreated = async (safeAddress): Promise<void> => { const onSafeCreated = async (safeAddress: string): Promise<void> => {
const pendingCreation = await loadFromStorage<LoadedSafeType>(SAFE_PENDING_CREATION_STORAGE_KEY) const pendingCreation = await loadFromStorage<LoadedSafeType>(SAFE_PENDING_CREATION_STORAGE_KEY)
let name = '' let name = ''

View File

@ -6,6 +6,7 @@ import { Modal } from 'src/components/Modal'
import { CSVReader } from 'react-papaparse' import { CSVReader } from 'react-papaparse'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { getWeb3 } from 'src/logic/wallets/getWeb3' import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { checksumAddress } from 'src/utils/checksumAddress'
const ImportContainer = styled.div` const ImportContainer = styled.div`
flex-direction: column; flex-direction: column;
@ -68,8 +69,8 @@ const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => {
} }
const formatedList = slicedData.map((entry) => { const formatedList = slicedData.map((entry) => {
const address = entry.data[0].toLowerCase() const address = entry.data[0]
return { address: getWeb3().utils.toChecksumAddress(address), name: entry.data[1] } return { address: checksumAddress(address), name: entry.data[1] }
}) })
setEntryList(formatedList) setEntryList(formatedList)
setImportError('') setImportError('')

View File

@ -126,7 +126,7 @@ const AddressBookTable = (): ReactElement => {
// close the modal // close the modal
setEditCreateEntryModalOpen(false) setEditCreateEntryModalOpen(false)
// update the store // update the store
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry))) dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
} }
const editEntryModalHandler = (entry: AddressBookEntry) => { const editEntryModalHandler = (entry: AddressBookEntry) => {
@ -135,7 +135,7 @@ const AddressBookTable = (): ReactElement => {
// close the modal // close the modal
setEditCreateEntryModalOpen(false) setEditCreateEntryModalOpen(false)
// update the store // update the store
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry))) dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
} }
const deleteEntryModalHandler = () => { const deleteEntryModalHandler = () => {
@ -149,11 +149,7 @@ const AddressBookTable = (): ReactElement => {
const importEntryModalHandler = (addressList: AddressBookEntry[]) => { const importEntryModalHandler = (addressList: AddressBookEntry[]) => {
addressList.forEach((entry) => { addressList.forEach((entry) => {
const checksumEntries = { dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry)))
...entry,
address: checksumAddress(entry.address),
}
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(checksumEntries)))
}) })
setImportEntryModalOpen(false) setImportEntryModalOpen(false)
} }

View File

@ -16,6 +16,7 @@ import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions' import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions'
import { NOTIFICATIONS } from 'src/logic/notifications' import { NOTIFICATIONS } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
import { getExplorerInfo } from 'src/config' import { getExplorerInfo } from 'src/config'
@ -27,18 +28,17 @@ export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
type OwnProps = { type OwnProps = {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
ownerAddress: string owner: OwnerData
selectedOwnerName: string
} }
export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => { export const EditOwnerModal = ({ isOpen, onClose, owner }: OwnProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const handleSubmit = ({ ownerName }: { ownerName: string }): void => { const handleSubmit = ({ ownerName }: { ownerName: string }): void => {
// Update the value only if the ownerName really changed // Update the value only if the ownerName really changed
if (ownerName !== selectedOwnerName) { if (ownerName !== owner.name) {
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: ownerAddress, name: ownerName }))) dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: owner.address, name: ownerName })))
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG)) dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
} }
onClose() onClose()
@ -70,7 +70,7 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
<Row margin="md"> <Row margin="md">
<Field <Field
component={TextField} component={TextField}
initialValue={selectedOwnerName} initialValue={owner.name}
name="ownerName" name="ownerName"
placeholder="Owner name*" placeholder="Owner name*"
testId={RENAME_OWNER_INPUT_TEST_ID} testId={RENAME_OWNER_INPUT_TEST_ID}
@ -82,10 +82,10 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
<Row> <Row>
<Block justify="center"> <Block justify="center">
<EthHashInfo <EthHashInfo
hash={ownerAddress} hash={owner.address}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(ownerAddress)} explorerUrl={getExplorerInfo(owner.address)}
/> />
</Block> </Block>
</Row> </Row>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { CheckOwner } from './screens/CheckOwner' import { CheckOwner } from './screens/CheckOwner'
import { ReviewRemoveOwnerModal } from './screens/Review' import { ReviewRemoveOwnerModal } from './screens/Review'
@ -13,9 +14,7 @@ import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selector
import { Dispatch } from 'src/logic/safe/store/actions/types.d' import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
type OwnerValues = { type OwnerValues = OwnerData & {
ownerAddress: string
ownerName: string
threshold: string threshold: string
} }
@ -52,18 +51,12 @@ export const sendRemoveOwner = async (
type RemoveOwnerProps = { type RemoveOwnerProps = {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
ownerAddress: string owner: OwnerData
ownerName: string
} }
export const RemoveOwnerModal = ({ export const RemoveOwnerModal = ({ isOpen, onClose, owner }: RemoveOwnerProps): React.ReactElement => {
isOpen,
onClose,
ownerAddress,
ownerName,
}: RemoveOwnerProps): React.ReactElement => {
const [activeScreen, setActiveScreen] = useState('checkOwner') const [activeScreen, setActiveScreen] = useState('checkOwner')
const [values, setValues] = useState<OwnerValues>({ ownerAddress, ownerName, threshold: '' }) const [values, setValues] = useState<OwnerValues>({ ...owner, threshold: '' })
const dispatch = useDispatch() const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
@ -94,7 +87,7 @@ export const RemoveOwnerModal = ({
const onRemoveOwner = (txParameters: TxParameters) => { const onRemoveOwner = (txParameters: TxParameters) => {
onClose() onClose()
sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters) sendRemoveOwner(values, safeAddress, owner.address, owner.name, dispatch, txParameters)
} }
return ( return (
@ -106,9 +99,7 @@ export const RemoveOwnerModal = ({
title="Remove owner from Safe" title="Remove owner from Safe"
> >
<> <>
{activeScreen === 'checkOwner' && ( {activeScreen === 'checkOwner' && <CheckOwner onClose={onClose} onSubmit={ownerSubmitted} owner={owner} />}
<CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
)}
{activeScreen === 'selectThreshold' && ( {activeScreen === 'selectThreshold' && (
<ThresholdForm <ThresholdForm
onClickBack={onClickBack} onClickBack={onClickBack}
@ -122,8 +113,7 @@ export const RemoveOwnerModal = ({
onClickBack={onClickBack} onClickBack={onClickBack}
onClose={onClose} onClose={onClose}
onSubmit={onRemoveOwner} onSubmit={onRemoveOwner}
ownerAddress={ownerAddress} owner={owner}
ownerName={ownerName}
threshold={Number(values.threshold)} threshold={Number(values.threshold)}
/> />
)} )}

View File

@ -7,6 +7,7 @@ import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
import { EthHashInfo } from '@gnosis.pm/safe-react-components' import { EthHashInfo } from '@gnosis.pm/safe-react-components'
@ -18,11 +19,10 @@ export const REMOVE_OWNER_MODAL_NEXT_BTN_TEST_ID = 'remove-owner-next-btn'
interface CheckOwnerProps { interface CheckOwnerProps {
onClose: () => void onClose: () => void
onSubmit: () => void onSubmit: () => void
ownerAddress: string owner: OwnerData
ownerName: string
} }
export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: CheckOwnerProps): ReactElement => { export const CheckOwner = ({ onClose, onSubmit, owner }: CheckOwnerProps): ReactElement => {
const classes = useStyles() const classes = useStyles()
return ( return (
@ -44,11 +44,11 @@ export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: Check
<Row> <Row>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={ownerAddress} hash={owner.address}
name={ownerName} name={owner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(ownerAddress)} explorerUrl={getExplorerInfo(owner.address)}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -19,6 +19,7 @@ import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName'
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
import { Modal } from 'src/components/Modal' import { Modal } from 'src/components/Modal'
@ -35,8 +36,7 @@ type ReviewRemoveOwnerProps = {
onClickBack: () => void onClickBack: () => void
onClose: () => void onClose: () => void
onSubmit: (txParameters: TxParameters) => void onSubmit: (txParameters: TxParameters) => void
ownerAddress: string owner: OwnerData
ownerName: string
threshold?: number threshold?: number
} }
@ -44,8 +44,7 @@ export const ReviewRemoveOwnerModal = ({
onClickBack, onClickBack,
onClose, onClose,
onSubmit, onSubmit,
ownerAddress, owner,
ownerName,
threshold = 1, threshold = 1,
}: ReviewRemoveOwnerProps): React.ReactElement => { }: ReviewRemoveOwnerProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
@ -91,9 +90,9 @@ export const ReviewRemoveOwnerModal = ({
// the data lookup can be removed from here // the data lookup can be removed from here
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
const safeOwners = await gnosisSafe.methods.getOwners().call() const safeOwners = await gnosisSafe.methods.getOwners().call()
const index = safeOwners.findIndex((owner) => sameAddress(owner, ownerAddress)) const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, owner.address))
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, threshold).encodeABI() const txData = gnosisSafe.methods.removeOwner(prevAddress, owner.address, threshold).encodeABI()
if (isCurrent) { if (isCurrent) {
setData(txData) setData(txData)
@ -107,7 +106,7 @@ export const ReviewRemoveOwnerModal = ({
return () => { return () => {
isCurrent = false isCurrent = false
} }
}, [safeAddress, ownerAddress, threshold]) }, [safeAddress, owner.address, threshold])
const closeEditModalCallback = (txParameters: TxParameters) => { const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted) const oldGasPrice = Number(gasPriceFormatted)
@ -186,17 +185,17 @@ export const ReviewRemoveOwnerModal = ({
</Row> </Row>
<Hairline /> <Hairline />
{owners?.map( {owners?.map(
(owner) => (safeOwner) =>
owner.address !== ownerAddress && ( !sameAddress(safeOwner.address, owner.address) && (
<React.Fragment key={owner.address}> <React.Fragment key={safeOwner.address}>
<Row className={classes.owner}> <Row className={classes.owner}>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={owner.address} hash={safeOwner.address}
name={owner.name} name={safeOwner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(owner.address)} explorerUrl={getExplorerInfo(safeOwner.address)}
/> />
</Col> </Col>
</Row> </Row>
@ -213,11 +212,11 @@ export const ReviewRemoveOwnerModal = ({
<Row className={classes.selectedOwner}> <Row className={classes.selectedOwner}>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={ownerAddress} hash={owner.address}
name={ownerName} name={owner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(ownerAddress)} explorerUrl={getExplorerInfo(owner.address)}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -15,14 +15,16 @@ import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm' import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm'
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review' import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { isValidAddress } from 'src/utils/isValidAddress'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
export type OwnerValues = { export type OwnerValues = {
newOwnerAddress: string address: string
newOwnerName: string name: string
} }
export const sendReplaceOwner = async ( export const sendReplaceOwner = async (
values: OwnerValues, newOwner: OwnerValues,
safeAddress: string, safeAddress: string,
ownerAddressToRemove: string, ownerAddressToRemove: string,
dispatch: Dispatch, dispatch: Dispatch,
@ -32,7 +34,7 @@ export const sendReplaceOwner = async (
const safeOwners = await gnosisSafe.methods.getOwners().call() const safeOwners = await gnosisSafe.methods.getOwners().call()
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove)) const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.newOwnerAddress).encodeABI() const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, newOwner.address).encodeABI()
const txHash = await dispatch( const txHash = await dispatch(
createTransaction({ createTransaction({
@ -49,45 +51,26 @@ export const sendReplaceOwner = async (
if (txHash) { if (txHash) {
// update the AB // update the AB
dispatch( dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
addressBookAddOrUpdate(
makeAddressBookEntry({
address: values.newOwnerAddress,
name: values.newOwnerName,
}),
),
)
} }
} }
type ReplaceOwnerProps = { type ReplaceOwnerProps = {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
ownerAddress: string owner: OwnerData
ownerName: string
} }
export const ReplaceOwnerModal = ({ export const ReplaceOwnerModal = ({ isOpen, onClose, owner }: ReplaceOwnerProps): React.ReactElement => {
isOpen,
onClose,
ownerAddress,
ownerName,
}: ReplaceOwnerProps): React.ReactElement => {
const [activeScreen, setActiveScreen] = useState('checkOwner') const [activeScreen, setActiveScreen] = useState('checkOwner')
const [values, setValues] = useState({ const [newOwner, setNewOwner] = useState({ address: '', name: '' })
newOwnerAddress: '',
newOwnerName: '',
})
const dispatch = useDispatch() const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
useEffect( useEffect(
() => () => { () => () => {
setActiveScreen('checkOwner') setActiveScreen('checkOwner')
setValues({ setNewOwner({ address: '', name: '' })
newOwnerAddress: '',
newOwnerName: '',
})
}, },
[isOpen], [isOpen],
) )
@ -96,22 +79,19 @@ export const ReplaceOwnerModal = ({
const ownerSubmitted = (newValues) => { const ownerSubmitted = (newValues) => {
const { ownerAddress, ownerName } = newValues const { ownerAddress, ownerName } = newValues
if (isValidAddress(ownerAddress)) {
const checksumAddr = checksumAddress(ownerAddress) const checksumAddr = checksumAddress(ownerAddress)
setValues({ setNewOwner({ address: checksumAddr, name: ownerName })
newOwnerAddress: checksumAddr,
newOwnerName: ownerName,
})
setActiveScreen('reviewReplaceOwner') setActiveScreen('reviewReplaceOwner')
} }
}
const onReplaceOwner = async (txParameters: TxParameters) => { const onReplaceOwner = async (txParameters: TxParameters) => {
onClose() onClose()
try { try {
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters) await sendReplaceOwner(newOwner, safeAddress, owner.address, dispatch, txParameters)
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
dispatch(
addressBookAddOrUpdate(makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName })),
)
} catch (error) { } catch (error) {
console.error('Error while removing an owner', error) console.error('Error while removing an owner', error)
} }
@ -127,22 +107,15 @@ export const ReplaceOwnerModal = ({
> >
<> <>
{activeScreen === 'checkOwner' && ( {activeScreen === 'checkOwner' && (
<OwnerForm <OwnerForm onClose={onClose} onSubmit={ownerSubmitted} initialValues={newOwner} owner={owner} />
onClose={onClose}
onSubmit={ownerSubmitted}
initialValues={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
/>
)} )}
{activeScreen === 'reviewReplaceOwner' && ( {activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwnerModal <ReviewReplaceOwnerModal
onClickBack={onClickBack} onClickBack={onClickBack}
onClose={onClose} onClose={onClose}
onSubmit={onReplaceOwner} onSubmit={onReplaceOwner}
ownerAddress={ownerAddress} owner={owner}
ownerName={ownerName} newOwner={newOwner}
values={values}
/> />
)} )}
</> </>

View File

@ -26,6 +26,7 @@ import { Modal } from 'src/components/Modal'
import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors' import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
import { getExplorerInfo, getNetworkId } from 'src/config' import { getExplorerInfo, getNetworkId } from 'src/config'
@ -59,18 +60,11 @@ type NewOwnerProps = {
type OwnerFormProps = { type OwnerFormProps = {
onClose: () => void onClose: () => void
onSubmit: (values: NewOwnerProps) => void onSubmit: (values: NewOwnerProps) => void
ownerAddress: string owner: OwnerData
ownerName: string
initialValues?: OwnerValues initialValues?: OwnerValues
} }
export const OwnerForm = ({ export const OwnerForm = ({ onClose, onSubmit, owner, initialValues }: OwnerFormProps): ReactElement => {
onClose,
onSubmit,
ownerAddress,
ownerName,
initialValues,
}: OwnerFormProps): ReactElement => {
const classes = useStyles() const classes = useStyles()
const handleSubmit = (values: NewOwnerProps) => { const handleSubmit = (values: NewOwnerProps) => {
@ -99,8 +93,8 @@ export const OwnerForm = ({
formMutators={formMutators} formMutators={formMutators}
onSubmit={handleSubmit} onSubmit={handleSubmit}
initialValues={{ initialValues={{
ownerName: initialValues?.newOwnerName, ownerName: initialValues?.name,
ownerAddress: initialValues?.newOwnerAddress, ownerAddress: initialValues?.address,
}} }}
> >
{(...args) => { {(...args) => {
@ -132,11 +126,11 @@ export const OwnerForm = ({
<Row className={classes.owner}> <Row className={classes.owner}>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={ownerAddress} hash={owner.address}
name={ownerName} name={owner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(ownerAddress)} explorerUrl={getExplorerInfo(owner.address)}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -24,6 +24,8 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara
import { Modal } from 'src/components/Modal' import { Modal } from 'src/components/Modal'
import { TransactionFees } from 'src/components/TransactionsFees' import { TransactionFees } from 'src/components/TransactionsFees'
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
@ -35,11 +37,10 @@ type ReplaceOwnerProps = {
onClose: () => void onClose: () => void
onClickBack: () => void onClickBack: () => void
onSubmit: (txParameters: TxParameters) => void onSubmit: (txParameters: TxParameters) => void
ownerAddress: string owner: OwnerData
ownerName: string newOwner: {
values: { address: string
newOwnerAddress: string name: string
newOwnerName: string
} }
} }
@ -47,9 +48,8 @@ export const ReviewReplaceOwnerModal = ({
onClickBack, onClickBack,
onClose, onClose,
onSubmit, onSubmit,
ownerAddress, owner,
ownerName, newOwner,
values,
}: ReplaceOwnerProps): React.ReactElement => { }: ReplaceOwnerProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const [data, setData] = useState('') const [data, setData] = useState('')
@ -85,9 +85,9 @@ export const ReviewReplaceOwnerModal = ({
const calculateReplaceOwnerData = async () => { const calculateReplaceOwnerData = async () => {
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
const safeOwners = await gnosisSafe.methods.getOwners().call() const safeOwners = await gnosisSafe.methods.getOwners().call()
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase()) const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, owner.address))
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI() const txData = gnosisSafe.methods.swapOwner(prevAddress, owner.address, newOwner.address).encodeABI()
if (isCurrent) { if (isCurrent) {
setData(txData) setData(txData)
} }
@ -97,7 +97,7 @@ export const ReviewReplaceOwnerModal = ({
return () => { return () => {
isCurrent = false isCurrent = false
} }
}, [ownerAddress, safeAddress, values.newOwnerAddress]) }, [owner.address, safeAddress, newOwner.address])
const closeEditModalCallback = (txParameters: TxParameters) => { const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted) const oldGasPrice = Number(gasPriceFormatted)
@ -174,17 +174,17 @@ export const ReviewReplaceOwnerModal = ({
</Row> </Row>
<Hairline /> <Hairline />
{owners?.map( {owners?.map(
(owner) => (safeOwner) =>
owner.address !== ownerAddress && ( !sameAddress(safeOwner.address, owner.address) && (
<React.Fragment key={owner.address}> <React.Fragment key={safeOwner.address}>
<Row className={classes.owner}> <Row className={classes.owner}>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={owner.address} hash={safeOwner.address}
name={owner.name} name={safeOwner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(owner.address)} explorerUrl={getExplorerInfo(safeOwner.address)}
/> />
</Col> </Col>
</Row> </Row>
@ -201,11 +201,11 @@ export const ReviewReplaceOwnerModal = ({
<Row className={classes.selectedOwnerRemoved}> <Row className={classes.selectedOwnerRemoved}>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={ownerAddress} hash={owner.address}
name={ownerName} name={owner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(ownerAddress)} explorerUrl={getExplorerInfo(owner.address)}
/> />
</Col> </Col>
</Row> </Row>
@ -218,11 +218,11 @@ export const ReviewReplaceOwnerModal = ({
<Row className={classes.selectedOwnerAdded}> <Row className={classes.selectedOwnerAdded}>
<Col align="center" xs={12}> <Col align="center" xs={12}>
<EthHashInfo <EthHashInfo
hash={values.newOwnerAddress} hash={newOwner.address}
name={values.newOwnerName} name={newOwner.name}
showCopyBtn showCopyBtn
showAvatar showAvatar
explorerUrl={getExplorerInfo(values.newOwnerAddress)} explorerUrl={getExplorerInfo(newOwner.address)}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -7,7 +7,9 @@ export const OWNERS_TABLE_NAME_ID = 'name'
export const OWNERS_TABLE_ADDRESS_ID = 'address' export const OWNERS_TABLE_ADDRESS_ID = 'address'
export const OWNERS_TABLE_ACTIONS_ID = 'actions' export const OWNERS_TABLE_ACTIONS_ID = 'actions'
export const getOwnerData = (owners: AddressBookState): { address: string; name: string }[] => { export type OwnerData = { address: string; name: string }
export const getOwnerData = (owners: AddressBookState): OwnerData[] => {
return owners.map((owner) => ({ return owners.map((owner) => ({
[OWNERS_TABLE_NAME_ID]: owner.name, [OWNERS_TABLE_NAME_ID]: owner.name,
[OWNERS_TABLE_ADDRESS_ID]: owner.address, [OWNERS_TABLE_ADDRESS_ID]: owner.address,

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect, ReactElement } from 'react'
import { EthHashInfo } from '@gnosis.pm/safe-react-components' import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import TableCell from '@material-ui/core/TableCell' import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer' import TableContainer from '@material-ui/core/TableContainer'
@ -13,7 +13,7 @@ import { RemoveOwnerModal } from './RemoveOwnerModal'
import { ReplaceOwnerModal } from './ReplaceOwnerModal' import { ReplaceOwnerModal } from './ReplaceOwnerModal'
import RenameOwnerIcon from './assets/icons/rename-owner.svg' import RenameOwnerIcon from './assets/icons/rename-owner.svg'
import ReplaceOwnerIcon from './assets/icons/replace-owner.svg' import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
import { OWNERS_TABLE_ADDRESS_ID, generateColumns, getOwnerData } from './dataFetcher' import { OWNERS_TABLE_ADDRESS_ID, generateColumns, getOwnerData, OwnerData } from './dataFetcher'
import { useStyles } from './style' import { useStyles } from './style'
import { getExplorerInfo } from 'src/config' import { getExplorerInfo } from 'src/config'
@ -41,12 +41,11 @@ type Props = {
owners: AddressBookState owners: AddressBookState
} }
const ManageOwners = ({ granted, owners }: Props): React.ReactElement => { const ManageOwners = ({ granted, owners }: Props): ReactElement => {
const { trackEvent } = useAnalytics() const { trackEvent } = useAnalytics()
const classes = useStyles() const classes = useStyles()
const [selectedOwnerAddress, setSelectedOwnerAddress] = useState('') const [selectedOwner, setSelectedOwner] = useState<OwnerData | undefined>()
const [selectedOwnerName, setSelectedOwnerName] = useState('')
const [modalsStatus, setModalStatus] = useState({ const [modalsStatus, setModalStatus] = useState({
showAddOwner: false, showAddOwner: false,
showRemoveOwner: false, showRemoveOwner: false,
@ -54,13 +53,14 @@ const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
showEditOwner: false, showEditOwner: false,
}) })
const onShow = (action, row?: any) => () => { const onShow = (action, row?: OwnerData) => () => {
setModalStatus((prevState) => ({ setModalStatus((prevState) => ({
...prevState, ...prevState,
[`show${action}`]: !prevState[`show${action}`], [`show${action}`]: !prevState[`show${action}`],
})) }))
setSelectedOwnerAddress(row && row.address) if (row) {
setSelectedOwnerName(row && row.name) setSelectedOwner(row)
}
} }
const onHide = (action) => () => { const onHide = (action) => () => {
@ -68,8 +68,7 @@ const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
...prevState, ...prevState,
[`show${action}`]: !Boolean(prevState[`show${action}`]), [`show${action}`]: !Boolean(prevState[`show${action}`]),
})) }))
setSelectedOwnerAddress('') setSelectedOwner(undefined)
setSelectedOwnerName('')
} }
useEffect(() => { useEffect(() => {
@ -180,24 +179,21 @@ const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
</> </>
)} )}
<AddOwnerModal isOpen={modalsStatus.showAddOwner} onClose={onHide('AddOwner')} /> <AddOwnerModal isOpen={modalsStatus.showAddOwner} onClose={onHide('AddOwner')} />
{selectedOwner && (
<>
<RemoveOwnerModal <RemoveOwnerModal
isOpen={modalsStatus.showRemoveOwner} isOpen={modalsStatus.showRemoveOwner}
onClose={onHide('RemoveOwner')} onClose={onHide('RemoveOwner')}
ownerAddress={selectedOwnerAddress} owner={selectedOwner}
ownerName={selectedOwnerName}
/> />
<ReplaceOwnerModal <ReplaceOwnerModal
isOpen={modalsStatus.showReplaceOwner} isOpen={modalsStatus.showReplaceOwner}
onClose={onHide('ReplaceOwner')} onClose={onHide('ReplaceOwner')}
ownerAddress={selectedOwnerAddress} owner={selectedOwner}
ownerName={selectedOwnerName}
/>
<EditOwnerModal
isOpen={modalsStatus.showEditOwner}
onClose={onHide('EditOwner')}
ownerAddress={selectedOwnerAddress}
selectedOwnerName={selectedOwnerName}
/> />
<EditOwnerModal isOpen={modalsStatus.showEditOwner} onClose={onHide('EditOwner')} owner={selectedOwner} />
</>
)}
</> </>
) )
} }

View File

@ -0,0 +1,20 @@
import { checksumAddress, isChecksumAddress } from '../checksumAddress'
describe('checksumAddress', () => {
it('Returns a checksummed address', () => {
const address = '0xbaddad0000000000000000000000000000000001'
const checksummedAddress = checksumAddress(address)
expect(checksummedAddress).toBe('0xbAddaD0000000000000000000000000000000001')
expect(isChecksumAddress(checksummedAddress)).toBeTruthy()
})
it('Throws if an invalid address was provided', () => {
const address = '0xbaddad'
try {
checksumAddress(address)
} catch (e) {
expect(e.message).toBe('Given address "0xbaddad" is not a valid Ethereum address.')
}
})
})

View File

@ -0,0 +1,19 @@
import { isValidAddress } from '../isValidAddress'
describe('isValidAddress', () => {
it('Returns false for an empty string', () => {
expect(isValidAddress('')).toBeFalsy()
})
it('Returns false when address is `undefined`', () => {
expect(isValidAddress(undefined)).toBeFalsy()
})
it('Returns false for `0x123`', () => {
expect(isValidAddress('0x123')).toBeFalsy()
})
it('Returns false for a valid address without `0x` prefix', () => {
expect(isValidAddress('0000000000000000000000000000000000000001')).toBeFalsy()
})
it('Returns true for a valid address with `0x` prefix', () => {
expect(isValidAddress('0x0000000000000000000000000000000000000001')).toBeTruthy()
})
})

View File

@ -1,5 +1,11 @@
import { getWeb3 } from 'src/logic/wallets/getWeb3' import { checkAddressChecksum, toChecksumAddress } from 'web3-utils'
export const checksumAddress = (address: string): string => { export const checksumAddress = (address: string): string => toChecksumAddress(address)
return getWeb3().utils.toChecksumAddress(address)
export const isChecksumAddress = (address?: string): boolean => {
if (address) {
return checkAddressChecksum(address)
}
return false
} }

View File

@ -0,0 +1,11 @@
import { isAddress, isHexStrict } from 'web3-utils'
export const isValidAddress = (address?: string): boolean => {
if (address) {
// `isAddress` do not require the string to start with `0x`
// `isHexStrict` ensures the address to start with `0x` aside from being a valid hex string
return isHexStrict(address) && isAddress(address)
}
return false
}