[AddressBook v2] - Avoid using `checksumAddress` inside reducer (#2355)
This commit is contained in:
parent
e58b6d6b7b
commit
fc0c450a74
|
@ -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
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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('')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.')
|
||||
}
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue