diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index fa601221..90ac6cde 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -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 }, ) diff --git a/src/logic/addressBook/store/reducer/index.ts b/src/logic/addressBook/store/reducer/index.ts index 8e7590d7..759b8a19 100644 --- a/src/logic/addressBook/store/reducer/index.ts +++ b/src/logic/addressBook/store/reducer/index.ts @@ -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( { [ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE]: (state, action: Action) => { 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( // 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) } }) diff --git a/src/routes/load/container/Load.tsx b/src/routes/load/container/Load.tsx index 96044d5e..1d415754 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -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 } diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index eac4a9be..771cf1f0 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -164,7 +164,7 @@ const Open = (): ReactElement => { setShowProgress(true) } - const onSafeCreated = async (safeAddress): Promise => { + const onSafeCreated = async (safeAddress: string): Promise => { const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) let name = '' diff --git a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx b/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx index 4aedb6b2..98b0870a 100644 --- a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx @@ -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('') diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index e259c4cc..1f90ab92 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -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) } diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index 01f79dc0..a423e420 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -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 diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index acbe2d75..ce215102 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -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({ ownerAddress, ownerName, threshold: '' }) + const [values, setValues] = useState({ ...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' && ( - - )} + {activeScreen === 'checkOwner' && } {activeScreen === 'selectThreshold' && ( )} diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx index 8ade7e58..6feaf2ea 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/CheckOwner/index.tsx @@ -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 diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index d2839495..067c54f9 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -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 = ({ {owners?.map( - (owner) => - owner.address !== ownerAddress && ( - + (safeOwner) => + !sameAddress(safeOwner.address, owner.address) && ( + @@ -213,11 +212,11 @@ export const ReviewRemoveOwnerModal = ({ diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index b43ea806..789ce192 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -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' && ( - + )} {activeScreen === 'reviewReplaceOwner' && ( )} diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx index 29f44a2d..1de744cb 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx @@ -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 = ({ diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index 54fbf898..5e3654a7 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -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 = ({ {owners?.map( - (owner) => - owner.address !== ownerAddress && ( - + (safeOwner) => + !sameAddress(safeOwner.address, owner.address) && ( + @@ -201,11 +201,11 @@ export const ReviewReplaceOwnerModal = ({ @@ -218,11 +218,11 @@ export const ReviewReplaceOwnerModal = ({ diff --git a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts index 171c7499..b77fdc99 100644 --- a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts +++ b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts @@ -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, diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index 1cc25060..11a57934 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -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() 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 => { )} - - - + {selectedOwner && ( + <> + + + + + )} ) } diff --git a/src/utils/__tests__/checksumAddress.test.ts b/src/utils/__tests__/checksumAddress.test.ts new file mode 100644 index 00000000..bda3d541 --- /dev/null +++ b/src/utils/__tests__/checksumAddress.test.ts @@ -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.') + } + }) +}) diff --git a/src/utils/__tests__/isValidAddress.test.ts b/src/utils/__tests__/isValidAddress.test.ts new file mode 100644 index 00000000..a24ca910 --- /dev/null +++ b/src/utils/__tests__/isValidAddress.test.ts @@ -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() + }) +}) diff --git a/src/utils/checksumAddress.ts b/src/utils/checksumAddress.ts index b0cd1fdc..a9207bac 100644 --- a/src/utils/checksumAddress.ts +++ b/src/utils/checksumAddress.ts @@ -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 } diff --git a/src/utils/isValidAddress.ts b/src/utils/isValidAddress.ts new file mode 100644 index 00000000..dc7fe03c --- /dev/null +++ b/src/utils/isValidAddress.ts @@ -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 +}