[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 { isFeatureEnabled } from 'src/config'
import { FEATURES } from 'src/config/networks/network.d'
import { isValidAddress } from 'src/utils/isValidAddress'
type ValidatorReturnType = string | undefined
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
@ -73,9 +74,7 @@ export const mustBeHexData = (data: string): ValidatorReturnType => {
export const mustBeAddressHash = memoize(
(address: string): ValidatorReturnType => {
const errorMessage = 'Must be a valid address'
const startsWith0x = address?.startsWith('0x')
const isAddress = getWeb3().utils.isAddress(address)
return startsWith0x && isAddress ? undefined : errorMessage
return isValidAddress(address) ? 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 { getEntryIndex, isValidAddressBookName } from 'src/logic/addressBook/utils'
import { AppReduxState } from 'src/store'
import { checksumAddress } from 'src/utils/checksumAddress'
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>) => {
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
return newState
}
// always checksum the address before storing it
const addressBookEntry = { address: checksumAddress(address), ...rest }
const entryIndex = getEntryIndex(newState, addressBookEntry)
// update
@ -56,18 +52,14 @@ export default handleActions<AppReduxState['addressBook'], Payloads>(
// exclude those entries with invalid name
.filter(({ name }) => isValidAddressBookName(name))
.forEach((addressBookEntry) => {
const { address, ...rest } = addressBookEntry
// always checksum the address before storing it
const newAddressBookEntry = { address: checksumAddress(address), ...rest }
const entryIndex = getEntryIndex(newState, newAddressBookEntry)
const entryIndex = getEntryIndex(newState, addressBookEntry)
if (entryIndex >= 0) {
// update
newState[entryIndex] = newAddressBookEntry
newState[entryIndex] = addressBookEntry
} else {
// 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 { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress'
import { isValidAddress } from 'src/utils/isValidAddress'
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
@ -59,7 +60,7 @@ const Load = (): ReactElement => {
const onLoadSafeSubmit = async (values: LoadFormValues) => {
let safeAddress = values[FIELD_LOAD_ADDRESS]
if (!safeAddress) {
if (!isValidAddress(safeAddress)) {
console.error('failed to add Safe address', JSON.stringify(values))
return
}

View File

@ -164,7 +164,7 @@ const Open = (): ReactElement => {
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)
let name = ''

View File

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

View File

@ -126,7 +126,7 @@ const AddressBookTable = (): ReactElement => {
// close the modal
setEditCreateEntryModalOpen(false)
// update the store
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry)))
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
}
const editEntryModalHandler = (entry: AddressBookEntry) => {
@ -135,7 +135,7 @@ const AddressBookTable = (): ReactElement => {
// close the modal
setEditCreateEntryModalOpen(false)
// update the store
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry)))
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ ...entry, address: checksumAddress(entry.address) })))
}
const deleteEntryModalHandler = () => {
@ -149,11 +149,7 @@ const AddressBookTable = (): ReactElement => {
const importEntryModalHandler = (addressList: AddressBookEntry[]) => {
addressList.forEach((entry) => {
const checksumEntries = {
...entry,
address: checksumAddress(entry.address),
}
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(checksumEntries)))
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry)))
})
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 { NOTIFICATIONS } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style'
import { getExplorerInfo } from 'src/config'
@ -27,18 +28,17 @@ export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
type OwnProps = {
isOpen: boolean
onClose: () => void
ownerAddress: string
selectedOwnerName: string
owner: OwnerData
}
export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => {
export const EditOwnerModal = ({ isOpen, onClose, owner }: OwnProps): React.ReactElement => {
const classes = useStyles()
const dispatch = useDispatch()
const handleSubmit = ({ ownerName }: { ownerName: string }): void => {
// Update the value only if the ownerName really changed
if (ownerName !== selectedOwnerName) {
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
if (ownerName !== owner.name) {
dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: owner.address, name: ownerName })))
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
}
onClose()
@ -70,7 +70,7 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
<Row margin="md">
<Field
component={TextField}
initialValue={selectedOwnerName}
initialValue={owner.name}
name="ownerName"
placeholder="Owner name*"
testId={RENAME_OWNER_INPUT_TEST_ID}
@ -82,10 +82,10 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
<Row>
<Block justify="center">
<EthHashInfo
hash={ownerAddress}
hash={owner.address}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(ownerAddress)}
explorerUrl={getExplorerInfo(owner.address)}
/>
</Block>
</Row>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { CheckOwner } from './screens/CheckOwner'
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 { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
type OwnerValues = {
ownerAddress: string
ownerName: string
type OwnerValues = OwnerData & {
threshold: string
}
@ -52,18 +51,12 @@ export const sendRemoveOwner = async (
type RemoveOwnerProps = {
isOpen: boolean
onClose: () => void
ownerAddress: string
ownerName: string
owner: OwnerData
}
export const RemoveOwnerModal = ({
isOpen,
onClose,
ownerAddress,
ownerName,
}: RemoveOwnerProps): React.ReactElement => {
export const RemoveOwnerModal = ({ isOpen, onClose, owner }: RemoveOwnerProps): React.ReactElement => {
const [activeScreen, setActiveScreen] = useState('checkOwner')
const [values, setValues] = useState<OwnerValues>({ ownerAddress, ownerName, threshold: '' })
const [values, setValues] = useState<OwnerValues>({ ...owner, threshold: '' })
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
@ -94,7 +87,7 @@ export const RemoveOwnerModal = ({
const onRemoveOwner = (txParameters: TxParameters) => {
onClose()
sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters)
sendRemoveOwner(values, safeAddress, owner.address, owner.name, dispatch, txParameters)
}
return (
@ -106,9 +99,7 @@ export const RemoveOwnerModal = ({
title="Remove owner from Safe"
>
<>
{activeScreen === 'checkOwner' && (
<CheckOwner onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
)}
{activeScreen === 'checkOwner' && <CheckOwner onClose={onClose} onSubmit={ownerSubmitted} owner={owner} />}
{activeScreen === 'selectThreshold' && (
<ThresholdForm
onClickBack={onClickBack}
@ -122,8 +113,7 @@ export const RemoveOwnerModal = ({
onClickBack={onClickBack}
onClose={onClose}
onSubmit={onRemoveOwner}
ownerAddress={ownerAddress}
ownerName={ownerName}
owner={owner}
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 Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style'
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 {
onClose: () => void
onSubmit: () => void
ownerAddress: string
ownerName: string
owner: OwnerData
}
export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: CheckOwnerProps): ReactElement => {
export const CheckOwner = ({ onClose, onSubmit, owner }: CheckOwnerProps): ReactElement => {
const classes = useStyles()
return (
@ -44,11 +44,11 @@ export const CheckOwner = ({ onClose, onSubmit, ownerAddress, ownerName }: Check
<Row>
<Col align="center" xs={12}>
<EthHashInfo
hash={ownerAddress}
name={ownerName}
hash={owner.address}
name={owner.name}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(ownerAddress)}
explorerUrl={getExplorerInfo(owner.address)}
/>
</Col>
</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 { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
import { OwnerData } from 'src/routes/safe/components/Settings/ManageOwners/dataFetcher'
import { useStyles } from './style'
import { Modal } from 'src/components/Modal'
@ -35,8 +36,7 @@ type ReviewRemoveOwnerProps = {
onClickBack: () => void
onClose: () => void
onSubmit: (txParameters: TxParameters) => void
ownerAddress: string
ownerName: string
owner: OwnerData
threshold?: number
}
@ -44,8 +44,7 @@ export const ReviewRemoveOwnerModal = ({
onClickBack,
onClose,
onSubmit,
ownerAddress,
ownerName,
owner,
threshold = 1,
}: ReviewRemoveOwnerProps): React.ReactElement => {
const classes = useStyles()
@ -91,9 +90,9 @@ export const ReviewRemoveOwnerModal = ({
// the data lookup can be removed from here
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
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 txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, threshold).encodeABI()
const txData = gnosisSafe.methods.removeOwner(prevAddress, owner.address, threshold).encodeABI()
if (isCurrent) {
setData(txData)
@ -107,7 +106,7 @@ export const ReviewRemoveOwnerModal = ({
return () => {
isCurrent = false
}
}, [safeAddress, ownerAddress, threshold])
}, [safeAddress, owner.address, threshold])
const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted)
@ -186,17 +185,17 @@ export const ReviewRemoveOwnerModal = ({
</Row>
<Hairline />
{owners?.map(
(owner) =>
owner.address !== ownerAddress && (
<React.Fragment key={owner.address}>
(safeOwner) =>
!sameAddress(safeOwner.address, owner.address) && (
<React.Fragment key={safeOwner.address}>
<Row className={classes.owner}>
<Col align="center" xs={12}>
<EthHashInfo
hash={owner.address}
name={owner.name}
hash={safeOwner.address}
name={safeOwner.name}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(owner.address)}
explorerUrl={getExplorerInfo(safeOwner.address)}
/>
</Col>
</Row>
@ -213,11 +212,11 @@ export const ReviewRemoveOwnerModal = ({
<Row className={classes.selectedOwner}>
<Col align="center" xs={12}>
<EthHashInfo
hash={ownerAddress}
name={ownerName}
hash={owner.address}
name={owner.name}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(ownerAddress)}
explorerUrl={getExplorerInfo(owner.address)}
/>
</Col>
</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 { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
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 = {
newOwnerAddress: string
newOwnerName: string
address: string
name: string
}
export const sendReplaceOwner = async (
values: OwnerValues,
newOwner: OwnerValues,
safeAddress: string,
ownerAddressToRemove: string,
dispatch: Dispatch,
@ -32,7 +34,7 @@ export const sendReplaceOwner = async (
const safeOwners = await gnosisSafe.methods.getOwners().call()
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
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(
createTransaction({
@ -49,45 +51,26 @@ export const sendReplaceOwner = async (
if (txHash) {
// update the AB
dispatch(
addressBookAddOrUpdate(
makeAddressBookEntry({
address: values.newOwnerAddress,
name: values.newOwnerName,
}),
),
)
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
}
}
type ReplaceOwnerProps = {
isOpen: boolean
onClose: () => void
ownerAddress: string
ownerName: string
owner: OwnerData
}
export const ReplaceOwnerModal = ({
isOpen,
onClose,
ownerAddress,
ownerName,
}: ReplaceOwnerProps): React.ReactElement => {
export const ReplaceOwnerModal = ({ isOpen, onClose, owner }: ReplaceOwnerProps): React.ReactElement => {
const [activeScreen, setActiveScreen] = useState('checkOwner')
const [values, setValues] = useState({
newOwnerAddress: '',
newOwnerName: '',
})
const [newOwner, setNewOwner] = useState({ address: '', name: '' })
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
useEffect(
() => () => {
setActiveScreen('checkOwner')
setValues({
newOwnerAddress: '',
newOwnerName: '',
})
setNewOwner({ address: '', name: '' })
},
[isOpen],
)
@ -96,22 +79,19 @@ export const ReplaceOwnerModal = ({
const ownerSubmitted = (newValues) => {
const { ownerAddress, ownerName } = newValues
const checksumAddr = checksumAddress(ownerAddress)
setValues({
newOwnerAddress: checksumAddr,
newOwnerName: ownerName,
})
setActiveScreen('reviewReplaceOwner')
if (isValidAddress(ownerAddress)) {
const checksumAddr = checksumAddress(ownerAddress)
setNewOwner({ address: checksumAddr, name: ownerName })
setActiveScreen('reviewReplaceOwner')
}
}
const onReplaceOwner = async (txParameters: TxParameters) => {
onClose()
try {
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters)
dispatch(
addressBookAddOrUpdate(makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName })),
)
await sendReplaceOwner(newOwner, safeAddress, owner.address, dispatch, txParameters)
dispatch(addressBookAddOrUpdate(makeAddressBookEntry(newOwner)))
} catch (error) {
console.error('Error while removing an owner', error)
}
@ -127,22 +107,15 @@ export const ReplaceOwnerModal = ({
>
<>
{activeScreen === 'checkOwner' && (
<OwnerForm
onClose={onClose}
onSubmit={ownerSubmitted}
initialValues={values}
ownerAddress={ownerAddress}
ownerName={ownerName}
/>
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} initialValues={newOwner} owner={owner} />
)}
{activeScreen === 'reviewReplaceOwner' && (
<ReviewReplaceOwnerModal
onClickBack={onClickBack}
onClose={onClose}
onSubmit={onReplaceOwner}
ownerAddress={ownerAddress}
ownerName={ownerName}
values={values}
owner={owner}
newOwner={newOwner}
/>
)}
</>

View File

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

View File

@ -24,6 +24,8 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara
import { Modal } from 'src/components/Modal'
import { TransactionFees } from 'src/components/TransactionsFees'
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'
@ -35,11 +37,10 @@ type ReplaceOwnerProps = {
onClose: () => void
onClickBack: () => void
onSubmit: (txParameters: TxParameters) => void
ownerAddress: string
ownerName: string
values: {
newOwnerAddress: string
newOwnerName: string
owner: OwnerData
newOwner: {
address: string
name: string
}
}
@ -47,9 +48,8 @@ export const ReviewReplaceOwnerModal = ({
onClickBack,
onClose,
onSubmit,
ownerAddress,
ownerName,
values,
owner,
newOwner,
}: ReplaceOwnerProps): React.ReactElement => {
const classes = useStyles()
const [data, setData] = useState('')
@ -85,9 +85,9 @@ export const ReviewReplaceOwnerModal = ({
const calculateReplaceOwnerData = async () => {
const gnosisSafe = getGnosisSafeInstanceAt(safeAddress)
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 txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI()
const txData = gnosisSafe.methods.swapOwner(prevAddress, owner.address, newOwner.address).encodeABI()
if (isCurrent) {
setData(txData)
}
@ -97,7 +97,7 @@ export const ReviewReplaceOwnerModal = ({
return () => {
isCurrent = false
}
}, [ownerAddress, safeAddress, values.newOwnerAddress])
}, [owner.address, safeAddress, newOwner.address])
const closeEditModalCallback = (txParameters: TxParameters) => {
const oldGasPrice = Number(gasPriceFormatted)
@ -174,17 +174,17 @@ export const ReviewReplaceOwnerModal = ({
</Row>
<Hairline />
{owners?.map(
(owner) =>
owner.address !== ownerAddress && (
<React.Fragment key={owner.address}>
(safeOwner) =>
!sameAddress(safeOwner.address, owner.address) && (
<React.Fragment key={safeOwner.address}>
<Row className={classes.owner}>
<Col align="center" xs={12}>
<EthHashInfo
hash={owner.address}
name={owner.name}
hash={safeOwner.address}
name={safeOwner.name}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(owner.address)}
explorerUrl={getExplorerInfo(safeOwner.address)}
/>
</Col>
</Row>
@ -201,11 +201,11 @@ export const ReviewReplaceOwnerModal = ({
<Row className={classes.selectedOwnerRemoved}>
<Col align="center" xs={12}>
<EthHashInfo
hash={ownerAddress}
name={ownerName}
hash={owner.address}
name={owner.name}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(ownerAddress)}
explorerUrl={getExplorerInfo(owner.address)}
/>
</Col>
</Row>
@ -218,11 +218,11 @@ export const ReviewReplaceOwnerModal = ({
<Row className={classes.selectedOwnerAdded}>
<Col align="center" xs={12}>
<EthHashInfo
hash={values.newOwnerAddress}
name={values.newOwnerName}
hash={newOwner.address}
name={newOwner.name}
showCopyBtn
showAvatar
explorerUrl={getExplorerInfo(values.newOwnerAddress)}
explorerUrl={getExplorerInfo(newOwner.address)}
/>
</Col>
</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_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) => ({
[OWNERS_TABLE_NAME_ID]: owner.name,
[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 TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
@ -13,7 +13,7 @@ import { RemoveOwnerModal } from './RemoveOwnerModal'
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
import RenameOwnerIcon from './assets/icons/rename-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 { getExplorerInfo } from 'src/config'
@ -41,12 +41,11 @@ type Props = {
owners: AddressBookState
}
const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
const ManageOwners = ({ granted, owners }: Props): ReactElement => {
const { trackEvent } = useAnalytics()
const classes = useStyles()
const [selectedOwnerAddress, setSelectedOwnerAddress] = useState('')
const [selectedOwnerName, setSelectedOwnerName] = useState('')
const [selectedOwner, setSelectedOwner] = useState<OwnerData | undefined>()
const [modalsStatus, setModalStatus] = useState({
showAddOwner: false,
showRemoveOwner: false,
@ -54,13 +53,14 @@ const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
showEditOwner: false,
})
const onShow = (action, row?: any) => () => {
const onShow = (action, row?: OwnerData) => () => {
setModalStatus((prevState) => ({
...prevState,
[`show${action}`]: !prevState[`show${action}`],
}))
setSelectedOwnerAddress(row && row.address)
setSelectedOwnerName(row && row.name)
if (row) {
setSelectedOwner(row)
}
}
const onHide = (action) => () => {
@ -68,8 +68,7 @@ const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
...prevState,
[`show${action}`]: !Boolean(prevState[`show${action}`]),
}))
setSelectedOwnerAddress('')
setSelectedOwnerName('')
setSelectedOwner(undefined)
}
useEffect(() => {
@ -180,24 +179,21 @@ const ManageOwners = ({ granted, owners }: Props): React.ReactElement => {
</>
)}
<AddOwnerModal isOpen={modalsStatus.showAddOwner} onClose={onHide('AddOwner')} />
<RemoveOwnerModal
isOpen={modalsStatus.showRemoveOwner}
onClose={onHide('RemoveOwner')}
ownerAddress={selectedOwnerAddress}
ownerName={selectedOwnerName}
/>
<ReplaceOwnerModal
isOpen={modalsStatus.showReplaceOwner}
onClose={onHide('ReplaceOwner')}
ownerAddress={selectedOwnerAddress}
ownerName={selectedOwnerName}
/>
<EditOwnerModal
isOpen={modalsStatus.showEditOwner}
onClose={onHide('EditOwner')}
ownerAddress={selectedOwnerAddress}
selectedOwnerName={selectedOwnerName}
/>
{selectedOwner && (
<>
<RemoveOwnerModal
isOpen={modalsStatus.showRemoveOwner}
onClose={onHide('RemoveOwner')}
owner={selectedOwner}
/>
<ReplaceOwnerModal
isOpen={modalsStatus.showReplaceOwner}
onClose={onHide('ReplaceOwner')}
owner={selectedOwner}
/>
<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 => {
return getWeb3().utils.toChecksumAddress(address)
export const checksumAddress = (address: string): string => 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
}