From 5ee9ef7b84e2af9b968fa19a876b286e8a78c249 Mon Sep 17 00:00:00 2001 From: Mati Dastugue Date: Mon, 10 May 2021 12:40:27 -0300 Subject: [PATCH 01/20] [Address Book] - Add Import/Export Buttons (#2278) * Add import/export buttons to addressBook * Change textSize to icons --- package.json | 2 +- .../safe/components/AddressBook/index.tsx | 35 ++++++++++++++++--- yarn.lock | 6 ++-- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 47fe0369..ec70dd8f 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#b281238", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#0e4fcd6", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.49.0", "@material-ui/core": "^4.11.0", diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index e033470c..21533407 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -1,4 +1,4 @@ -import { Button, EthHashInfo, FixedIcon, Text } from '@gnosis.pm/safe-react-components' +import { Button, EthHashInfo, FixedIcon, Text, ButtonLink } from '@gnosis.pm/safe-react-components' import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' @@ -14,7 +14,6 @@ import { getExplorerInfo } from 'src/config' import Table from 'src/components/Table' import { cellWidth } from 'src/components/Table/TableHead' import Block from 'src/components/layout/Block' -import ButtonLink from 'src/components/layout/ButtonLink' import Col from 'src/components/layout/Col' import Img from 'src/components/layout/Img' import Row from 'src/components/layout/Row' @@ -147,10 +146,36 @@ const AddressBookTable = (): ReactElement => { setSelectedEntry(initialEntryState) setEditCreateEntryModalOpen(true) }} - size="lg" - testId="manage-tokens-btn" + color="primary" + iconType="exportImg" + iconSize="sm" + textSize="xl" > - + Create entry + Export + + { + setSelectedEntry(initialEntryState) + setEditCreateEntryModalOpen(true) + }} + color="primary" + iconType="importImg" + iconSize="sm" + textSize="xl" + > + Import + + { + setSelectedEntry(initialEntryState) + setEditCreateEntryModalOpen(true) + }} + color="primary" + iconType="add" + iconSize="sm" + textSize="xl" + > + Create entry diff --git a/yarn.lock b/yarn.lock index 8ba924fa..a79e41ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1637,9 +1637,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#b281238": - version "0.5.0" - resolved "https://github.com/gnosis/safe-react-components.git#b2812381a265e9b0a17abbc11392986e6c1c74b8" +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#0e4fcd6": + version "0.6.0" + resolved "https://github.com/gnosis/safe-react-components.git#0e4fcd619e15bb0b854195430edcac64feacc8e2" dependencies: classnames "^2.2.6" react-media "^1.10.0" From 839c947e828cca46c82cee0a4b5b34df80bf9925 Mon Sep 17 00:00:00 2001 From: Mati Dastugue Date: Mon, 17 May 2021 11:55:12 -0300 Subject: [PATCH 02/20] [Address Book V2] - Export address book (#2284) * Add export feature * Improve Address book export modal * Use styled-components instead of material-ui styles * Add comment * Improve filename + set loading state false when error --- package.json | 1 + src/components/Modal/index.tsx | 20 ++- .../notifications/notificationBuilder.tsx | 15 ++ src/logic/notifications/notificationTypes.ts | 10 ++ .../safe/transactions/notifiedTransactions.ts | 1 + .../ExportEntriesModal/assets/error.svg | 9 ++ .../ExportEntriesModal/assets/success.svg | 8 + .../ExportEntriesModal/assets/wait.svg | 8 + .../AddressBook/ExportEntriesModal/index.tsx | 145 ++++++++++++++++++ .../safe/components/AddressBook/index.tsx | 5 +- yarn.lock | 20 +++ 11 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg create mode 100644 src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg create mode 100644 src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg create mode 100644 src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx diff --git a/package.json b/package.json index ec70dd8f..38bc13b6 100644 --- a/package.json +++ b/package.json @@ -214,6 +214,7 @@ "react-ga": "3.3.0", "react-hot-loader": "4.13.0", "react-intersection-observer": "^8.31.0", + "react-papaparse": "^3.14.0", "react-qr-reader": "^2.2.1", "react-redux": "7.2.3", "react-router-dom": "5.2.0", diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 5a6dcc33..9ba7d185 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -184,13 +184,21 @@ const Body = ({ children, withoutPadding = false }: BodyProps): ReactElement => ) /*** Footer ***/ -const FooterSection = styled.div` +const FooterSection = styled.div<{ + withoutPadding: FooterProps['withoutPadding'] + withoutBorder: FooterProps['withoutBorder'] +}>` display: flex; justify-content: center; - border-top: 2px solid ${({ theme }) => theme.colors.separator}; - padding: 24px; + border-top: ${({ withoutBorder }) => (withoutBorder ? 0 : `2px solid ${({ theme }) => theme.colors.separator}`)}; + padding: ${({ withoutPadding }) => (withoutPadding ? 0 : '24px')}; ` +interface FooterProps { + withoutPadding?: boolean + withoutBorder?: boolean +} + const ButtonStyled = styled(Button)` &.MuiButtonBase-root { margin: 0 10px; @@ -240,8 +248,10 @@ interface FooterProps { children: ReactNode | ReactNodeArray } -const Footer = ({ children }: FooterProps): ReactElement => ( - {children} +const Footer = ({ children, withoutPadding = false, withoutBorder = false }: FooterProps): ReactElement => ( + + {children} + ) Footer.Buttons = Buttons diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index fae0a821..91249a22 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -153,6 +153,17 @@ const addressBookDeleteEntry = { afterExecutionError: null, } +const addressBookExportEntries = { + beforeExecution: null, + afterRejection: null, + waitingConfirmation: null, + afterExecution: { + noMoreConfirmationsNeeded: NOTIFICATIONS.ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS, + moreConfirmationsNeeded: null, + }, + afterExecutionError: NOTIFICATIONS.ADDRESS_BOOK_EXPORT_ENTRIES_ERROR, +} + export const getNotificationsFromTxType: any = (txType, origin) => { let notificationsQueue @@ -205,6 +216,10 @@ export const getNotificationsFromTxType: any = (txType, origin) => { notificationsQueue = addressBookDeleteEntry break } + case TX_NOTIFICATION_TYPES.ADDRESSBOOK_EXPORT_ENTRIES: { + notificationsQueue = addressBookExportEntries + break + } default: { notificationsQueue = defaultNotificationsQueue break diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index efc6a302..f01b0d27 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -52,6 +52,8 @@ const NOTIFICATION_IDS = { ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS', ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS', ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: 'ADDRESS_BOOK_DELETE_ENTRY_SUCCESS', + ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS: 'ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS', + ADDRESS_BOOK_EXPORT_ENTRIES_ERROR: 'ADDRESS_BOOK_EXPORT_ENTRIES_ERROR', SAFE_NEW_VERSION_AVAILABLE: 'SAFE_NEW_VERSION_AVAILABLE', } @@ -206,6 +208,14 @@ export const NOTIFICATIONS: Record = { message: 'Entry deleted successfully', options: { variant: SUCCESS, persist: false, preventDuplicate: false }, }, + ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS: { + message: 'Address book exported', + options: { variant: SUCCESS, persist: false, preventDuplicate: false }, + }, + ADDRESS_BOOK_EXPORT_ENTRIES_ERROR: { + message: 'An error occurred while generating the address book CSV.', + options: { variant: ERROR, persist: false, preventDuplicate: false }, + }, // Safe Version SAFE_NEW_VERSION_AVAILABLE: { diff --git a/src/logic/safe/transactions/notifiedTransactions.ts b/src/logic/safe/transactions/notifiedTransactions.ts index f166bbab..9f18650e 100644 --- a/src/logic/safe/transactions/notifiedTransactions.ts +++ b/src/logic/safe/transactions/notifiedTransactions.ts @@ -11,4 +11,5 @@ export const TX_NOTIFICATION_TYPES = { ADDRESSBOOK_NEW_ENTRY: 'ADDRESSBOOK_NEW_ENTRY', ADDRESSBOOK_EDIT_ENTRY: 'ADDRESSBOOK_EDIT_ENTRY', ADDRESSBOOK_DELETE_ENTRY: 'ADDRESSBOOK_DELETE_ENTRY', + ADDRESSBOOK_EXPORT_ENTRIES: 'ADDRESSBOOK_EXPORT_ENTRIES', } diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg new file mode 100644 index 00000000..0b2b8dd4 --- /dev/null +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/error.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg new file mode 100644 index 00000000..09f27d51 --- /dev/null +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/success.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg new file mode 100644 index 00000000..9b8677de --- /dev/null +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/assets/wait.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx new file mode 100644 index 00000000..dfd022b2 --- /dev/null +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx @@ -0,0 +1,145 @@ +import React, { ReactElement, useEffect, useState } from 'react' +import { format } from 'date-fns' +import { useSelector, useDispatch } from 'react-redux' +import { CSVDownloader, jsonToCSV } from 'react-papaparse' +import { Button, Loader, Text } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' + +import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' +import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' +import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' + +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' + +import { lg, md, background } from 'src/theme/variables' + +import { Modal } from 'src/components/Modal' +import Img from 'src/components/layout/Img' +import Row from 'src/components/layout/Row' + +import SuccessSvg from './assets/success.svg' +import ErrorSvg from './assets/error.svg' +import LoadingSvg from './assets/wait.svg' + +type ExportEntriesModalProps = { + isOpen: boolean + onClose: () => void +} + +const ImageContainer = styled(Row)` + padding: ${md} ${lg}; + justify-content: center; +` + +const InfoContainer = styled(Row)` + background-color: ${background}; + flex-direction: column; + justify-content: center; + padding: ${lg}; + text-align: center; +` + +const BodyImage = styled.div` + grid-row: 1; +` +const StyledLoader = styled(Loader)` + margin-right: 5px; +` +const StyledCSVLink = styled(CSVDownloader)` + display: flex; + flex: 1; + justify-content: center; +` + +export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps): ReactElement => { + const dispatch = useDispatch() + const addressBook: AddressBookState = useSelector(addressBookSelector) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + const [csvData, setCsvData] = useState('') + const [doRetry, setDoRetry] = useState(false) + + const date = format(new Date(), 'yyyy-MM-dd') + + const handleClose = () => + //This timeout prevents modal to be closed abruptly + setTimeout(() => { + if (!loading) { + const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EXPORT_ENTRIES) + const action = error + ? notification.afterExecution.afterExecutionError + : notification.afterExecution.noMoreConfirmationsNeeded + dispatch(enqueueSnackbar(enhanceSnackbarForAction(action))) + } + onClose() + }, 600) + + useEffect(() => { + const handleCsvData = () => { + if (!isOpen && !doRetry) return + setLoading(true) + setError('') + try { + setCsvData(jsonToCSV(addressBook)) + } catch (e) { + setLoading(false) + setError(e.message) + return + } + setLoading(false) + setDoRetry(false) + } + + handleCsvData() + }, [addressBook, isOpen, doRetry, csvData]) + + return ( + + + Export address book + + + + + Export + + + + + {!error ? ( + + You're about to export a CSV file with{' '} + + {addressBook.length} address book entries + + . + + ) : ( + + An error occurred while generating the address book CSV. + + )} + + + + + + + + + + + ) +} diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 21533407..74986921 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -24,6 +24,7 @@ import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/upda import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses' import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal' +import { ExportEntriesModal } from 'src/routes/safe/components/AddressBook/ExportEntriesModal' import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal' import { AB_ADDRESS_ID, @@ -77,6 +78,7 @@ const AddressBookTable = (): ReactElement => { const [selectedEntry, setSelectedEntry] = useState(initialEntryState) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) + const [exportEntriesModalOpen, setExportEntriesModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) const { trackEvent } = useAnalytics() @@ -144,7 +146,7 @@ const AddressBookTable = (): ReactElement => { { setSelectedEntry(initialEntryState) - setEditCreateEntryModalOpen(true) + setExportEntriesModalOpen(true) }} color="primary" iconType="exportImg" @@ -283,6 +285,7 @@ const AddressBookTable = (): ReactElement => { isOpen={deleteEntryModalOpen} onClose={() => setDeleteEntryModalOpen(false)} /> + setExportEntriesModalOpen(false)} /> Date: Mon, 17 May 2021 17:06:08 +0200 Subject: [PATCH 03/20] [Address book v2] Address book update events (#2293) * Remove UNKNOWN from owner list * Fetch Safe Name from address book * Update Safe name in address book when updating from settings > safe details * Fix lint issue --- src/components/App/index.tsx | 4 ++-- .../SafeListSidebar/SafeList/AddressWrapper.tsx | 5 ++--- src/components/SafeListSidebar/index.tsx | 16 +++++++++++++++- src/logic/addressBook/hooks/useSafeName.tsx | 10 ++++++++++ src/logic/addressBook/utils/index.ts | 4 ++-- src/logic/safe/store/selectors/index.ts | 2 -- .../load/components/DetailsForm/index.tsx | 2 +- .../components/Apps/components/AppFrame.tsx | 9 +++------ .../Apps/hooks/useIframeMessageHandler.ts | 10 ++++------ src/routes/safe/components/Balances/index.tsx | 13 +++++-------- .../AddOwnerModal/screens/Review/index.tsx | 9 +++++---- .../RemoveOwnerModal/screens/Review/index.tsx | 5 +++-- .../ReplaceOwnerModal/screens/Review/index.tsx | 4 ++-- .../components/Settings/ManageOwners/index.tsx | 4 ++-- .../Settings/RemoveSafeModal/index.tsx | 11 ++++------- .../components/Settings/SafeDetails/index.tsx | 17 +++++++++-------- 16 files changed, 69 insertions(+), 56 deletions(-) create mode 100644 src/logic/addressBook/hooks/useSafeName.tsx diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 3c9c8507..aea11d5b 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -22,13 +22,13 @@ import { networkSelector } from 'src/logic/wallets/store/selectors' import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' import { safeTotalFiatBalanceSelector, - safeNameSelector, safeParamAddressFromStateSelector, safeLoadedViaUrlSelector, } from 'src/logic/safe/store/selectors' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import Modal from 'src/components/Modal' import SendModal from 'src/routes/safe/components/Balances/SendModal' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe' import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates' import useSafeActions from 'src/logic/safe/hooks/useSafeActions' @@ -72,7 +72,7 @@ const App: React.FC = ({ children }) => { const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false }) const history = useHistory() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSelector(safeNameSelector) ?? '' + const safeName = useSafeName(safeAddress) const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions() const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector) const currentCurrency = useSelector(currentCurrencySelector) diff --git a/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx b/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx index 0be0668d..bb18da6b 100644 --- a/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx +++ b/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactElement } from 'react' import styled from 'styled-components' import { ButtonLink, EthHashInfo, Text } from '@gnosis.pm/safe-react-components' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' @@ -51,9 +51,8 @@ type Props = { const { nativeCoin } = getNetworkInfo() -export const AddressWrapper = (props: Props): React.ReactElement => { +export const AddressWrapper = ({ safe, defaultSafe }: Props): ReactElement => { const classes = useStyles() - const { safe, defaultSafe } = props const dispatch = useDispatch() const setDefaultSafeAction = (safeAddress: string) => { diff --git a/src/components/SafeListSidebar/index.tsx b/src/components/SafeListSidebar/index.tsx index 7050ca8e..f6680520 100644 --- a/src/components/SafeListSidebar/index.tsx +++ b/src/components/SafeListSidebar/index.tsx @@ -1,3 +1,4 @@ +import { List } from 'immutable' import React, { useEffect, useMemo, useState, ReactElement } from 'react' import Drawer from '@material-ui/core/Drawer' import SearchIcon from '@material-ui/icons/Search' @@ -18,6 +19,9 @@ import { WELCOME_ADDRESS } from 'src/routes/routes' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' export const SafeListSidebarContext = React.createContext({ isOpen: false, @@ -31,6 +35,15 @@ const filterBy = (filter, safes) => safe.address.toLowerCase().includes(filter.toLowerCase()) || safe.name.toLowerCase().includes(filter.toLowerCase()), ) +const useAddressBookSafeNames = (safeList: List): List => { + const addressBook = useSelector(addressBookSelector) + + return safeList.map((safeRecord) => { + const safe = safeRecord.toObject() + const name = getNameFromAddressBook(addressBook, safe.address) || '' + return { ...safe, name } + }) +} type Props = { children: ReactElement @@ -39,9 +52,10 @@ type Props = { export const SafeListSidebar = ({ children }: Props): ReactElement => { const [isOpen, setIsOpen] = useState(false) const [filter, setFilter] = useState('') - const safes = useSelector(sortedSafeListSelector).filter((safe) => !safe.loadedViaUrl) + const safesFromStore = useSelector(sortedSafeListSelector).filter((safe) => !safe.loadedViaUrl) const defaultSafe = useSelector(defaultSafeSelector) const currentSafe = useSelector(safeParamAddressFromStateSelector) + const safes = useAddressBookSafeNames(safesFromStore) const classes = useSidebarStyles() const { trackEvent } = useAnalytics() diff --git a/src/logic/addressBook/hooks/useSafeName.tsx b/src/logic/addressBook/hooks/useSafeName.tsx new file mode 100644 index 00000000..ac9d37e8 --- /dev/null +++ b/src/logic/addressBook/hooks/useSafeName.tsx @@ -0,0 +1,10 @@ +import { useSelector } from 'react-redux' + +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' + +export const useSafeName = (safeAddress: string): string => { + const addressBook = useSelector(addressBookSelector) + + return getNameFromAddressBook(addressBook, safeAddress) || '' +} diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index f4e835de..345dbad3 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -94,7 +94,7 @@ export const getOwnersWithNameFromAddressBook = ( const ownerName = getNameFromAddressBook(addressBook, owner.address) return { address: owner.address, - name: ownerName || owner.name, + name: ownerName || '', } }) } @@ -116,7 +116,7 @@ export const formatAddressListToAddressBookNames = ( } /** - * If the safe is not loaded, the owner wasn't not deleted + * If the safe is not loaded, the owner wasn't deleted * If the safe is already loaded and the owner has a valid name, will return true if the address is not already on the addressBook * @param name * @param address diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 895bf4ed..274bdb3a 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -83,8 +83,6 @@ export const safeFieldSelector = (field: K) => safe: SafeRecord, ): SafeRecordProps[K] | undefined => (safe ? safe.get(field, baseSafe.get(field)) : undefined) -export const safeNameSelector = createSelector(safeSelector, safeFieldSelector('name')) - export const safeEthBalanceSelector = createSelector(safeSelector, safeFieldSelector('ethBalance')) export const safeNeedsUpdateSelector = createSelector(safeSelector, safeFieldSelector('needsUpdate')) diff --git a/src/routes/load/components/DetailsForm/index.tsx b/src/routes/load/components/DetailsForm/index.tsx index 16ad1de1..9a0e1fe3 100644 --- a/src/routes/load/components/DetailsForm/index.tsx +++ b/src/routes/load/components/DetailsForm/index.tsx @@ -92,7 +92,7 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => { const granted = useSelector(grantedSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) const ethBalance = useSelector(safeEthBalanceSelector) - const safeName = useSelector(safeNameSelector) + const safeName = useSafeName(safeAddress) const { trackEvent } = useAnalytics() const history = useHistory() const { consentReceived, onConsentReceipt } = useLegalConsent() diff --git a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts index a87a3ab8..3a38f672 100644 --- a/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts +++ b/src/routes/safe/components/Apps/hooks/useIframeMessageHandler.ts @@ -12,12 +12,10 @@ import { } from '@gnosis.pm/safe-apps-sdk-v1' import { useDispatch, useSelector } from 'react-redux' import { useEffect, useCallback, MutableRefObject } from 'react' + import { getNetworkName, getTxServiceUrl } from 'src/config/' -import { - safeEthBalanceSelector, - safeNameSelector, - safeParamAddressFromStateSelector, -} from 'src/logic/safe/store/selectors' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' +import { safeEthBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TransactionParams } from '../components/AppFrame' import { SafeApp } from 'src/routes/safe/components/Apps/types.d' @@ -39,8 +37,8 @@ const useIframeMessageHandler = ( iframeRef: MutableRefObject, ): ReturnType => { const { enqueueSnackbar, closeSnackbar } = useSnackbar() - const safeName = useSelector(safeNameSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSafeName(safeAddress) const ethBalance = useSelector(safeEthBalanceSelector) const dispatch = useDispatch() diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index 0470abc3..8f62becf 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core/styles' -import React, { useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import ReceiveModal from 'src/components/App/ReceiveModal' @@ -13,11 +13,8 @@ import Row from 'src/components/layout/Row' import { SAFELIST_ADDRESS } from 'src/routes/routes' import SendModal from 'src/routes/safe/components/Balances/SendModal' import { CurrencyDropdown } from 'src/routes/safe/components/CurrencyDropdown' -import { - safeFeaturesEnabledSelector, - safeNameSelector, - safeParamAddressFromStateSelector, -} from 'src/logic/safe/store/selectors' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' +import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { wrapInSuspense } from 'src/utils/wrapInSuspense' import { useFetchTokens } from 'src/logic/safe/hooks/useFetchTokens' @@ -45,13 +42,13 @@ export const COLLECTIBLES_LOCATION_REGEX = /\/balances\/collectibles$/ const useStyles = makeStyles(styles) -const Balances = (): React.ReactElement => { +const Balances = (): ReactElement => { const classes = useStyles() const [state, setState] = useState(INITIAL_STATE) const address = useSelector(safeParamAddressFromStateSelector) const featuresEnabled = useSelector(safeFeaturesEnabledSelector) - const safeName = useSelector(safeNameSelector) ?? '' + const safeName = useSafeName(address) useFetchTokens(address) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index af8d551d..e630d723 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -1,7 +1,7 @@ import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import React, { useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { EthHashInfo } from '@gnosis.pm/safe-react-components' @@ -13,7 +13,8 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' +import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' @@ -34,11 +35,11 @@ type ReviewAddOwnerProps = { values: OwnerValues } -export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): React.ReactElement => { +export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): ReactElement => { const classes = useStyles() const [data, setData] = useState('') const safeAddress = useSelector(safeParamAddressFromStateSelector) as string - const safeName = useSelector(safeNameSelector) + const safeName = useSafeName(safeAddress) const owners = useSelector(safeOwnersSelector) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() 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 9a5fbddf..13db0932 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 @@ -14,7 +14,8 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' -import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' @@ -50,7 +51,7 @@ export const ReviewRemoveOwnerModal = ({ const classes = useStyles() const [data, setData] = useState('') const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSelector(safeNameSelector) + const safeName = useSafeName(safeAddress) const owners = useSelector(safeOwnersSelector) const addressBook = useSelector(addressBookSelector) const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) 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 7ce44ee6..e1a1f83c 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 @@ -15,11 +15,11 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { - safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector, safeThresholdSelector, } from 'src/logic/safe/store/selectors' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' @@ -57,7 +57,7 @@ export const ReviewReplaceOwnerModal = ({ const classes = useStyles() const [data, setData] = useState('') const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSelector(safeNameSelector) + const safeName = useSafeName(safeAddress) const owners = useSelector(safeOwnersSelector) const threshold = useSelector(safeThresholdSelector) || 1 const addressBook = useSelector(addressBookSelector) diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index 11097f9e..229e9d9a 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -15,7 +15,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, OWNERS_TABLE_NAME_ID, generateColumns, getOwnerData } from './dataFetcher' +import { OWNERS_TABLE_ADDRESS_ID, generateColumns, getOwnerData } from './dataFetcher' import { styles } from './style' import { getExplorerInfo } from 'src/config' @@ -103,7 +103,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme columns={columns} data={ownerData} defaultFixed - defaultOrderBy={OWNERS_TABLE_NAME_ID} + defaultOrderBy={OWNERS_TABLE_ADDRESS_ID} disablePagination label="Owners" noBorder diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx index e2ce4238..b63de7af 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx @@ -1,3 +1,4 @@ +import { EthHashInfo } from '@gnosis.pm/safe-react-components' import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' @@ -6,17 +7,13 @@ import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' -import { EthHashInfo } from '@gnosis.pm/safe-react-components' import Modal from 'src/components/Modal' import Block from 'src/components/layout/Block' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { - defaultSafeSelector, - safeNameSelector, - safeParamAddressFromStateSelector, -} from 'src/logic/safe/store/selectors' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' +import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { WELCOME_ADDRESS } from 'src/routes/routes' import { removeLocalSafe } from 'src/logic/safe/store/actions/removeLocalSafe' import { sameAddress } from 'src/logic/wallets/ethAddresses' @@ -36,7 +33,7 @@ type RemoveSafeModalProps = { export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): React.ReactElement => { const classes = useStyles() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSelector(safeNameSelector) + const safeName = useSafeName(safeAddress) const defaultSafe = useSelector(defaultSafeSelector) const dispatch = useDispatch() diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index 1a5ccbd2..8ff58cee 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core/styles' -import React, { useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' @@ -15,20 +15,21 @@ import Col from 'src/components/layout/Col' import Heading from 'src/components/layout/Heading' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' +import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { UpdateSafeModal } from 'src/routes/safe/components/Settings/UpdateSafeModal' import { grantedSelector } from 'src/routes/safe/container/selector' -import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' +import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' import { latestMasterContractVersionSelector, safeCurrentVersionSelector, - safeNameSelector, safeNeedsUpdateSelector, safeParamAddressFromStateSelector, } from 'src/logic/safe/store/selectors' @@ -51,18 +52,18 @@ const StyledIcon = styled(Icon)` left: 6px; ` -const SafeDetails = (): React.ReactElement => { +const SafeDetails = (): ReactElement => { const classes = useStyles() const isUserOwner = useSelector(grantedSelector) const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector) const dispatch = useDispatch() - const safeName = useSelector(safeNameSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const safeName = useSafeName(safeAddress) const safeNeedsUpdate = useSelector(safeNeedsUpdateSelector) const safeCurrentVersion = useSelector(safeCurrentVersionSelector) const { trackEvent } = useAnalytics() - const [isModalOpen, setModalOpen] = React.useState(false) - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [isModalOpen, setModalOpen] = useState(false) const [safeInfo, setSafeInfo] = useState() const toggleModal = () => { @@ -70,7 +71,7 @@ const SafeDetails = (): React.ReactElement => { } const handleSubmit = (values) => { - dispatch(updateSafe({ address: safeAddress, name: values.safeName })) + dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: safeAddress, name: values.safeName }))) const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX) dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) From 28afa59d48565a7b448b9fa9b1c9fee8105a90c1 Mon Sep 17 00:00:00 2001 From: juampibermani <30930241+juampibermani@users.noreply.github.com> Date: Fri, 21 May 2021 09:41:40 -0300 Subject: [PATCH 04/20] [Address Book V2] Import address book (#2290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added import modal and import functionality * Added modal refactor * Added entry validations * Added message after loading file * Reset values on modal close * Minor formal changes * Entry list validations on separate functions * Minor changes * Text corrections * Check if isAddress instead of checksum * add styles on import * fix sidebar label for ADDRESS BOOK (with a space) * fix modal title in lowecase * replace icons on AB list * Clear upload feedback on modal close * add styles to drag and drop area * add link to Help Center article * remove unused styles * change color when is an error * change error color * fix error color and align file size * Modal refactor * fix button styles on modal * fix long file names Co-authored-by: Agustín Longoni --- .../AppLayout/Sidebar/useSidebarItems.tsx | 2 +- .../CreateEditEntryModal/index.tsx | 151 ++++++------ .../AddressBook/CreateEditEntryModal/style.ts | 1 + .../AddressBook/DeleteEntryModal/index.tsx | 2 +- .../AddressBook/ExportEntriesModal/index.tsx | 23 +- .../AddressBook/ImportEntryModal/index.tsx | 226 ++++++++++++++++++ .../safe/components/AddressBook/index.tsx | 70 ++++-- 7 files changed, 370 insertions(+), 105 deletions(-) create mode 100644 src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx diff --git a/src/components/AppLayout/Sidebar/useSidebarItems.tsx b/src/components/AppLayout/Sidebar/useSidebarItems.tsx index c31b4cbe..e7e3c89d 100644 --- a/src/components/AppLayout/Sidebar/useSidebarItems.tsx +++ b/src/components/AppLayout/Sidebar/useSidebarItems.tsx @@ -56,7 +56,7 @@ const useSidebarItems = (): ListItemType[] => { href: `${matchSafeWithAddress?.url}/transactions`, }, { - label: 'AddressBook', + label: 'ADDRESS BOOK', icon: , selected: matchSafeWithAction?.params.safeAction === 'address-book', href: `${matchSafeWithAddress?.url}/address-book`, diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx index cc37955f..9243e547 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -1,11 +1,9 @@ -import IconButton from '@material-ui/core/IconButton' -import Close from '@material-ui/icons/Close' import React, { ReactElement } from 'react' import { useSelector } from 'react-redux' import { useStyles } from './style' -import Modal from 'src/components/Modal' +import { Modal } from 'src/components/Modal' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' @@ -15,8 +13,6 @@ import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/co import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' 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 { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors' import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' @@ -67,88 +63,81 @@ export const CreateEditEntryModal = ({ description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'} handleClose={onClose} open={isOpen} - paperClassName="smaller-modal-window" title={isNew ? 'Create new entry' : 'Edit entry'} > - - - {isNew ? 'Create entry' : 'Edit entry'} - - - - - - - - {(...args) => { - const formState = args[2] - const mutators = args[3] - const handleScan = (value, closeQrModal) => { - let scannedAddress = value + + {isNew ? 'Create entry' : 'Edit entry'} + + + + {(...args) => { + const formState = args[2] + const mutators = args[3] + const handleScan = (value, closeQrModal) => { + let scannedAddress = value - if (scannedAddress.startsWith('ethereum:')) { - scannedAddress = scannedAddress.replace('ethereum:', '') + if (scannedAddress.startsWith('ethereum:')) { + scannedAddress = scannedAddress.replace('ethereum:', '') + } + + mutators.setOwnerAddress(scannedAddress) + closeQrModal() } - - mutators.setOwnerAddress(scannedAddress) - closeQrModal() - } - return ( - <> - - - - - - - - - (isNew ? isUniqueAddress(value) : undefined)]} - /> - - {isNew ? ( - - + return ( + <> + + + + - ) : null} + + + + (isNew ? isUniqueAddress(value) : undefined)]} + /> + + {isNew ? ( + + + + ) : null} + + + + + - - - - - - - - ) - }} - + + ) + }} + + ) } diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts b/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts index 8aca604c..9bcf667e 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/style.ts @@ -22,6 +22,7 @@ export const useStyles = makeStyles( buttonRow: { height: '84px', justifyContent: 'center', + gap: '16px', }, }), ) diff --git a/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx b/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx index 91390839..625a4c3d 100644 --- a/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/DeleteEntryModal/index.tsx @@ -30,7 +30,7 @@ const DeleteEntryModalComponent = ({ classes, deleteEntryModalHandler, entryToDe > - Delete Entry + Delete entry diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx index dfd022b2..660bf3f0 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx @@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useState } from 'react' import { format } from 'date-fns' import { useSelector, useDispatch } from 'react-redux' import { CSVDownloader, jsonToCSV } from 'react-papaparse' -import { Button, Loader, Text } from '@gnosis.pm/safe-react-components' +import { Button, Icon, Link, Loader, Text } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' @@ -52,6 +52,14 @@ const StyledCSVLink = styled(CSVDownloader)` justify-content: center; ` +const StyledIcon = styled(Icon)` + svg { + position: relative; + top: 4px; + left: 4px; + } +` + export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps): ReactElement => { const dispatch = useDispatch() const addressBook: AddressBookState = useSelector(addressBookSelector) @@ -111,7 +119,18 @@ export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps) You're about to export a CSV file with{' '} - {addressBook.length} address book entries + {addressBook.length} address book entries.
+ + + Learn more about importing / exporting an address book. + + +
.
diff --git a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx b/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx new file mode 100644 index 00000000..4aedb6b2 --- /dev/null +++ b/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx @@ -0,0 +1,226 @@ +import React, { useState } from 'react' + +import styled from 'styled-components' +import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' +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' + +const ImportContainer = styled.div` + flex-direction: column; + justify-content: center; + margin: 24px; + align-items: center; + /* width: 200px;*/ + min-height: 100px; + display: flex; +` + +const InfoContainer = styled.div` + background-color: ${({ theme }) => theme.colors.background}; + flex-direction: column; + justify-content: center; + padding: 24px; + text-align: center; + margin-top: 16px; +` +const StyledIcon = styled(Icon)` + svg { + position: relative; + top: 4px; + left: 4px; + } +` + +const WRONG_FILE_EXTENSION_ERROR = 'Only CSV files are allowed' +const FILE_SIZE_TOO_BIG = 'The size of the file is over 1 MB' +const FILE_BYTES_LIMIT = 1000000 +const IMPORT_SUPPORTED_FORMATS = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', +] + +const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => { + const [csvLoaded, setCsvLoaded] = useState(false) + const [importError, setImportError] = useState('') + const [entryList, setEntryList] = useState([]) + + const handleImportEntrySubmit = () => { + setCsvLoaded(false) + importEntryModalHandler(entryList) + } + + const handleOnDrop = (data, file) => { + const slicedData = data.slice(1) + + const fileError = validateFile(file) + if (fileError) { + setImportError(fileError) + return + } + + const dataError = validateCsvData(slicedData) + if (dataError) { + setImportError(dataError) + return + } + + const formatedList = slicedData.map((entry) => { + const address = entry.data[0].toLowerCase() + return { address: getWeb3().utils.toChecksumAddress(address), name: entry.data[1] } + }) + setEntryList(formatedList) + setImportError('') + setCsvLoaded(true) + } + + const validateFile = (file) => { + if (!IMPORT_SUPPORTED_FORMATS.includes(file.type)) { + return WRONG_FILE_EXTENSION_ERROR + } + + if (file.size >= FILE_BYTES_LIMIT) { + return FILE_SIZE_TOO_BIG + } + + return + } + + const validateCsvData = (data) => { + for (let index = 0; index < data.length; index++) { + const entry = data[index] + if (!entry.data[0] || !entry.data[1]) { + return `Invalid amount of columns on row ${index + 2}` + } + // Verify address properties + const address = entry.data[0].toLowerCase() + if (!getWeb3().utils.isAddress(address)) { + return `Invalid address on row ${index + 2}` + } + return + } + } + + const handleOnError = (error) => { + setImportError(error.message) + } + + const handleOnRemoveFile = () => { + setCsvLoaded(false) + setImportError('') + } + + const handleClose = () => { + setCsvLoaded(false) + setEntryList([]) + setImportError('') + onClose() + } + + return ( + + + Import address book + + + + theme.colors.primary}', */ + }, + dropFile: { + width: 200, + height: 100, + background: '#fff', + boxShadow: 'rgb(40 54 61 / 18%) 1px 2px 10px 0px', + borderRadius: 8, + }, + fileSizeInfo: { + color: '#001428', + lineHeight: 1, + position: 'absolute', + left: '10px', + top: '12px', + }, + fileNameInfo: { + color: importError === '' ? '#008C73' : '#DB3A3D', + backgroundColor: '#fff', + fontSize: 14, + lineHeight: 1.4, + padding: '0 0.4em', + margin: '1.2em 0 0.5em 0', + maxHeight: '59px', + overflow: 'hidden', + }, + progressBar: { + backgroundColor: '#008C73', + }, + removeButton: { + color: '#DB3A3D', + }, + }} + > + + Drop your CSV file here
+ or click to upload. +
+
+
+ + {importError !== '' && ( + + {importError} + + )} + {!csvLoaded && importError === '' && ( + + Only CSV files are allowed in the format [Address, Name] separated by comma.
+ + + Learn more about importing / exporting an address book. + + + +
+ )} + {csvLoaded && importError === '' && ( + <> + {`You're about to import`} + {` ${entryList.length} entries to your address book`} + + )} +
+
+ + handleClose() }} + confirmButtonProps={{ + color: 'primary', + disabled: !csvLoaded || importError !== '', + onClick: handleImportEntrySubmit, + text: 'Import', + }} + /> + +
+ ) +} + +export default ImportEntryModal diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 74986921..99ee50b2 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -1,4 +1,4 @@ -import { Button, EthHashInfo, FixedIcon, Text, ButtonLink } from '@gnosis.pm/safe-react-components' +import { Button, EthHashInfo, FixedIcon, Text, ButtonLink, Icon } from '@gnosis.pm/safe-react-components' import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' @@ -15,7 +15,6 @@ import Table from 'src/components/Table' import { cellWidth } from 'src/components/Table/TableHead' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' -import Img from 'src/components/layout/Img' import Row from 'src/components/layout/Row' import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry' @@ -29,18 +28,15 @@ import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntry import { AB_ADDRESS_ID, ADDRESS_BOOK_ROW_ID, - EDIT_ENTRY_BUTTON, - REMOVE_ENTRY_BUTTON, SEND_ENTRY_BUTTON, generateColumns, } from 'src/routes/safe/components/AddressBook/columns' import SendModal from 'src/routes/safe/components/Balances/SendModal' -import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg' -import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg' import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' import { grantedSelector } from 'src/routes/safe/container/selector' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import ImportEntryModal from './ImportEntryModal' const StyledButton = styled(Button)` &&.MuiButton-root { @@ -52,6 +48,17 @@ const StyledButton = styled(Button)` margin: 0 6px 0 0; } ` +const UnStyledButton = styled.button` + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline-color: ${({ theme }) => theme.colors.icon}; + display: flex; + align-items: center; +` const useStyles = makeStyles(styles) interface AddressBookSelectedEntry extends AddressBookEntry { @@ -77,6 +84,7 @@ const AddressBookTable = (): ReactElement => { const granted = useSelector(grantedSelector) const [selectedEntry, setSelectedEntry] = useState(initialEntryState) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) + const [importEntryModalOpen, setImportEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [exportEntriesModalOpen, setExportEntriesModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) @@ -139,6 +147,17 @@ const AddressBookTable = (): ReactElement => { dispatch(removeAddressBookEntry(entryAddress)) } + const importEntryModalHandler = (addressList: AddressBookEntry[]) => { + addressList.forEach((entry) => { + const checksumEntries = { + ...entry, + address: checksumAddress(entry.address), + } + dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries))) + }) + setImportEntryModalOpen(false) + } + return ( <> @@ -157,8 +176,7 @@ const AddressBookTable = (): ReactElement => {
{ - setSelectedEntry(initialEntryState) - setEditCreateEntryModalOpen(true) + setImportEntryModalOpen(true) }} color="primary" iconType="importImg" @@ -223,9 +241,7 @@ const AddressBookTable = (): ReactElement => { })} - Edit entry { setSelectedEntry({ entry: row, @@ -233,19 +249,28 @@ const AddressBookTable = (): ReactElement => { }) setEditCreateEntryModalOpen(true) }} - src={RenameOwnerIcon} - testId={EDIT_ENTRY_BUTTON} - /> - Remove entry + + + { setSelectedEntry({ entry: row }) setDeleteEntryModalOpen(true) }} - src={RemoveOwnerIcon} - testId={REMOVE_ENTRY_BUTTON} - /> + > + + {granted ? ( { onClose={() => setDeleteEntryModalOpen(false)} /> setExportEntriesModalOpen(false)} /> + setImportEntryModalOpen(false)} + /> Date: Wed, 26 May 2021 12:25:01 -0300 Subject: [PATCH 05/20] [Address Book v2] Make the address book global (#2296) * WIP: add `chainId` with default value to `AddressBookEntry` * WIP: first approach to the new AB structure and data persistence in localStorage * migrate AB * migrate AB actions * migrate AB reducers * update AB selectors * update AB middlewares * update AB utils * update AB model file * migrate AB usage to the new structure * change AB notification key name * fixes after rebase * add `addressBookBatchLoad` action * create a selector for owners with AB data `safeOwnersWithAddressBookDataSelector` * migrate safe->owners structure * make `name` and `address` mandatory for `makeAddressBookEntry` * filter out invalid AB names before storing them * review Load & Open flow for owner's names * update owners' reducers and actions - `editSafeOwner` no longer necessary as there's no name change in the owners' list * create AB map-like selector chainId -> address -> name * auto-complete owner's name if it exist in the AB * update replace owner modal and remove REPLACE_SAFE_OWNER - no longer optimistically update the owner's list if threshold === 1, as it behaves erratically with the constant update from data provided by the services - `REPLACE_SAFE_OWNER` no longer necessary * update remove owner modal and remove REMOVE_SAFE_OWNER - no longer optimistically update the owner's list if threshold === 1, as it behaves erratically with the constant update from data provided by the services - `REMOVE_SAFE_OWNER` no longer necessary * update add owner modal and remove ADD_SAFE_OWNER - no longer optimistically update the owner's list if tx is signed, as it behaves erratically with the constant update from data provided by the services - `ADD_SAFE_OWNER` no longer necessary * update yarn.lock after rebase * fix AddressBookMap type * create a migration function for safe's names * create selectors that require safe's name * remove `name` from the safe model and update related code * JSON.parse if JSON.parsed string returned a string instead of a JSON * remove commented lines * rename BATCH_LOAD to SAFE_LOAD * refactor AddressBookMap to `chainId`->`address`->`AddressBookEntry` * make `chainId` optional for `getNameFromAddressBookSelector` * use `addressBookMapSelector` for a more direct AddressBook entry name lookup * remove `loadedSafeViaUrl` flag, and rely on address book entry instead * replace `addAddressBookEntry` action with `addressBookAddOrUpdate` --- package.json | 1 + src/components/App/index.tsx | 9 +- .../SafeList/AddressWrapper.tsx | 13 +- src/components/SafeListSidebar/index.tsx | 16 +- src/components/SafeListSidebar/selectors.ts | 9 +- src/components/forms/validator.ts | 3 +- src/config/networks/network.d.ts | 4 +- src/logic/addressBook/model/addressBook.ts | 21 +- .../store/actions/addAddressBookEntry.ts | 22 -- .../actions/addOrUpdateAddressBookEntry.ts | 8 - src/logic/addressBook/store/actions/index.ts | 17 ++ .../store/actions/loadAddressBook.ts | 8 - .../actions/loadAddressBookFromStorage.ts | 22 -- .../store/actions/removeAddressBookEntry.ts | 7 - .../store/actions/updateAddressBookEntry.ts | 8 - .../store/middleware/addressBookMiddleware.ts | 61 ----- .../addressBook/store/middleware/index.ts | 37 +++ .../addressBook/store/reducer/addressBook.ts | 78 ------- src/logic/addressBook/store/reducer/index.ts | 82 +++++++ .../addressBook/store/selectors/index.ts | 63 +++-- .../utils/__tests__/addressBookUtils.test.ts | 167 +------------ src/logic/addressBook/utils/index.ts | 220 +++++++++++++----- .../notifications/notificationBuilder.tsx | 8 +- src/logic/safe/hooks/useLoadSafe.tsx | 11 +- .../store/actions/__tests__/fetchSafe.test.ts | 65 +++--- .../store/actions/__tests__/utils.test.ts | 109 +++------ .../safe/store/actions/addOrUpdateSafe.ts | 10 +- src/logic/safe/store/actions/addSafeOwner.ts | 5 - src/logic/safe/store/actions/editSafeOwner.ts | 7 - src/logic/safe/store/actions/fetchSafe.ts | 21 +- .../store/actions/mocks/safeInformation.ts | 65 ++---- .../safe/store/actions/removeSafeOwner.ts | 5 - .../safe/store/actions/replaceSafeOwner.ts | 5 - src/logic/safe/store/actions/utils.ts | 13 +- .../safe/store/middleware/safeStorage.ts | 53 +---- src/logic/safe/store/models/owner.ts | 8 - src/logic/safe/store/models/safe.ts | 16 +- src/logic/safe/store/reducer/safe.ts | 77 +----- src/logic/safe/store/selectors/index.ts | 66 ++++-- .../safe/transactions/notifiedTransactions.ts | 8 +- .../shouldSafeStoreBeUpdated.test.ts | 53 +---- src/logic/safe/utils/safeStorage.ts | 2 +- .../safe/utils/shouldSafeStoreBeUpdated.ts | 2 +- .../wallets/__tests__/ethAddresses.test.ts | 15 +- src/logic/wallets/ethAddresses.ts | 2 +- src/routes/load/container/Load.tsx | 47 ++-- src/routes/open/container/Open.tsx | 38 ++- src/routes/open/utils/safeDataExtractor.ts | 23 +- .../AddressBook/ExportEntriesModal/index.tsx | 2 +- .../safe/components/AddressBook/index.tsx | 36 +-- .../Balances/SendModal/SafeInfo/index.tsx | 6 +- .../screens/AddressBookInput/index.tsx | 25 +- .../ManageOwners/AddOwnerModal/index.tsx | 9 +- .../AddOwnerModal/screens/OwnerForm/index.tsx | 30 ++- .../AddOwnerModal/screens/Review/index.tsx | 15 +- .../screens/ThresholdForm/index.tsx | 12 +- .../ManageOwners/EditOwnerModal/index.tsx | 10 +- .../ManageOwners/RemoveOwnerModal/index.tsx | 13 +- .../RemoveOwnerModal/screens/Review/index.tsx | 25 +- .../screens/ThresholdForm/index.tsx | 5 +- .../ManageOwners/ReplaceOwnerModal/index.tsx | 28 +-- .../screens/OwnerForm/index.tsx | 34 ++- .../screens/Review/index.tsx | 19 +- .../Settings/ManageOwners/dataFetcher.ts | 5 +- .../Settings/ManageOwners/index.tsx | 15 +- .../Settings/RemoveSafeModal/index.tsx | 14 +- .../components/Settings/SafeDetails/index.tsx | 7 +- .../SpendingLimit/InfoDisplay/AddressInfo.tsx | 2 +- .../ChangeThreshold/index.tsx | 10 +- .../Settings/ThresholdSettings/index.tsx | 11 +- src/routes/safe/components/Settings/index.tsx | 13 +- .../Transactions/TxList/OwnerRow.tsx | 8 +- .../Transactions/TxList/TxInfoDetails.tsx | 2 +- .../Transactions/TxList/TxOwners.tsx | 4 +- .../TxList/hooks/useKnownAddress.ts | 2 +- src/store/index.ts | 19 +- yarn.lock | 126 ++-------- 77 files changed, 850 insertions(+), 1267 deletions(-) delete mode 100644 src/logic/addressBook/store/actions/addAddressBookEntry.ts delete mode 100644 src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts create mode 100644 src/logic/addressBook/store/actions/index.ts delete mode 100644 src/logic/addressBook/store/actions/loadAddressBook.ts delete mode 100644 src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts delete mode 100644 src/logic/addressBook/store/actions/removeAddressBookEntry.ts delete mode 100644 src/logic/addressBook/store/actions/updateAddressBookEntry.ts delete mode 100644 src/logic/addressBook/store/middleware/addressBookMiddleware.ts create mode 100644 src/logic/addressBook/store/middleware/index.ts delete mode 100644 src/logic/addressBook/store/reducer/addressBook.ts create mode 100644 src/logic/addressBook/store/reducer/index.ts delete mode 100644 src/logic/safe/store/actions/addSafeOwner.ts delete mode 100644 src/logic/safe/store/actions/editSafeOwner.ts delete mode 100644 src/logic/safe/store/actions/removeSafeOwner.ts delete mode 100644 src/logic/safe/store/actions/replaceSafeOwner.ts delete mode 100644 src/logic/safe/store/models/owner.ts diff --git a/package.json b/package.json index db5bc271..64b323e2 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "react-window": "^1.8.6", "redux": "4.0.5", "redux-actions": "^2.6.5", + "redux-localstorage-simple": "^2.4.0", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", "semver": "^7.3.2", diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index aea11d5b..00e814f9 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -20,11 +20,7 @@ import { getNetworkId } from 'src/config' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { networkSelector } from 'src/logic/wallets/store/selectors' import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' -import { - safeTotalFiatBalanceSelector, - safeParamAddressFromStateSelector, - safeLoadedViaUrlSelector, -} from 'src/logic/safe/store/selectors' +import { safeTotalFiatBalanceSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import Modal from 'src/components/Modal' import SendModal from 'src/routes/safe/components/Balances/SendModal' @@ -78,8 +74,7 @@ const App: React.FC = ({ children }) => { const currentCurrency = useSelector(currentCurrencySelector) const granted = useSelector(grantedSelector) const sidebarItems = useSidebarItems() - const isSafeLoadedViaUrl = useSelector(safeLoadedViaUrlSelector) - const safeLoaded = useLoadSafe(safeAddress, isSafeLoadedViaUrl) + const safeLoaded = useLoadSafe(safeAddress) useSafeScheduledUpdates(safeLoaded, safeAddress) const sendFunds = safeActionsState.sendFunds diff --git a/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx b/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx index bb18da6b..494b449b 100644 --- a/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx +++ b/src/components/SafeListSidebar/SafeList/AddressWrapper.tsx @@ -1,15 +1,17 @@ -import React, { ReactElement } from 'react' -import styled from 'styled-components' import { ButtonLink, EthHashInfo, Text } from '@gnosis.pm/safe-react-components' +import { makeStyles } from '@material-ui/core/styles' +import React, { ReactElement } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components' + +import { safeNameSelector } from 'src/logic/safe/store/selectors' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { sameAddress } from 'src/logic/wallets/ethAddresses' import DefaultBadge from './DefaultBadge' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe' import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe' -import { makeStyles } from '@material-ui/core/styles' import { getNetworkInfo } from 'src/config' -import { useDispatch } from 'react-redux' const StyledButtonLink = styled(ButtonLink)` visibility: hidden; @@ -54,6 +56,7 @@ const { nativeCoin } = getNetworkInfo() export const AddressWrapper = ({ safe, defaultSafe }: Props): ReactElement => { const classes = useStyles() const dispatch = useDispatch() + const safeName = useSelector((state) => safeNameSelector(state, safe.address)) const setDefaultSafeAction = (safeAddress: string) => { dispatch(setDefaultSafe(safeAddress)) @@ -61,7 +64,7 @@ export const AddressWrapper = ({ safe, defaultSafe }: Props): ReactElement => { return (
- +
{`${formatAmount(safe.ethBalance)} ${nativeCoin.name}`} diff --git a/src/components/SafeListSidebar/index.tsx b/src/components/SafeListSidebar/index.tsx index f6680520..0e263ae7 100644 --- a/src/components/SafeListSidebar/index.tsx +++ b/src/components/SafeListSidebar/index.tsx @@ -1,4 +1,3 @@ -import { List } from 'immutable' import React, { useEffect, useMemo, useState, ReactElement } from 'react' import Drawer from '@material-ui/core/Drawer' import SearchIcon from '@material-ui/icons/Search' @@ -19,9 +18,6 @@ import { WELCOME_ADDRESS } from 'src/routes/routes' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' -import { addressBookSelector } from 'src/logic/addressBook/store/selectors' -import { getNameFromAddressBook } from 'src/logic/addressBook/utils' export const SafeListSidebarContext = React.createContext({ isOpen: false, @@ -35,15 +31,6 @@ const filterBy = (filter, safes) => safe.address.toLowerCase().includes(filter.toLowerCase()) || safe.name.toLowerCase().includes(filter.toLowerCase()), ) -const useAddressBookSafeNames = (safeList: List): List => { - const addressBook = useSelector(addressBookSelector) - - return safeList.map((safeRecord) => { - const safe = safeRecord.toObject() - const name = getNameFromAddressBook(addressBook, safe.address) || '' - return { ...safe, name } - }) -} type Props = { children: ReactElement @@ -52,10 +39,9 @@ type Props = { export const SafeListSidebar = ({ children }: Props): ReactElement => { const [isOpen, setIsOpen] = useState(false) const [filter, setFilter] = useState('') - const safesFromStore = useSelector(sortedSafeListSelector).filter((safe) => !safe.loadedViaUrl) + const safes = useSelector(sortedSafeListSelector) const defaultSafe = useSelector(defaultSafeSelector) const currentSafe = useSelector(safeParamAddressFromStateSelector) - const safes = useAddressBookSafeNames(safesFromStore) const classes = useSidebarStyles() const { trackEvent } = useAnalytics() diff --git a/src/components/SafeListSidebar/selectors.ts b/src/components/SafeListSidebar/selectors.ts index 66066a74..8c9ccad3 100644 --- a/src/components/SafeListSidebar/selectors.ts +++ b/src/components/SafeListSidebar/selectors.ts @@ -1,7 +1,10 @@ import { createSelector } from 'reselect' -import { safesListSelector } from 'src/logic/safe/store/selectors' +import { safesListWithAddressBookNameSelector } from 'src/logic/safe/store/selectors' -export const sortedSafeListSelector = createSelector(safesListSelector, (safes) => - safes.sort((a, b) => (a.name > b.name ? 1 : -1)), +/** + * Sort safe list by the name in the address book + */ +export const sortedSafeListSelector = createSelector([safesListWithAddressBookNameSelector], (safes) => + safes.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)), ) diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 28211b89..8a7c79b5 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -1,4 +1,3 @@ -import { List } from 'immutable' import memoize from 'lodash.memoize' import { sameAddress } from 'src/logic/wallets/ethAddresses' @@ -98,7 +97,7 @@ export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value: export const ADDRESS_REPEATED_ERROR = 'Address already introduced' export const OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR = 'Cannot use Safe itself as owner.' -export const uniqueAddress = (addresses: string[] | List = []) => (address?: string): string | undefined => { +export const uniqueAddress = (addresses: string[] = []) => (address?: string): string | undefined => { const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address)) return addressExists ? ADDRESS_REPEATED_ERROR : undefined } diff --git a/src/config/networks/network.d.ts b/src/config/networks/network.d.ts index 6e4ac6ad..99e7919f 100644 --- a/src/config/networks/network.d.ts +++ b/src/config/networks/network.d.ts @@ -34,6 +34,7 @@ type Token = { } export enum ETHEREUM_NETWORK { + UNKNOWN = 0, MAINNET = 1, MORDEN = 2, ROPSTEN = 3, @@ -42,9 +43,8 @@ export enum ETHEREUM_NETWORK { KOVAN = 42, XDAI = 100, ENERGY_WEB_CHAIN = 246, - VOLTA = 73799, - UNKNOWN = 0, LOCAL = 4447, + VOLTA = 73799, } export type NetworkSettings = { diff --git a/src/logic/addressBook/model/addressBook.ts b/src/logic/addressBook/model/addressBook.ts index 8575448f..d6ddcaeb 100644 --- a/src/logic/addressBook/model/addressBook.ts +++ b/src/logic/addressBook/model/addressBook.ts @@ -1,17 +1,28 @@ +import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { getNetworkId } from 'src/config' + +export const ADDRESS_BOOK_DEFAULT_NAME = 'UNKNOWN' + export type AddressBookEntry = { - address: string - name: string + address: string // the contact address + name: string // human-readable name + chainId: ETHEREUM_NETWORK // see https://chainid.network } +const networkId = getNetworkId() + export const makeAddressBookEntry = ({ - address = '', - name = '', + address, + name, + chainId = networkId, }: { address: string - name?: string + name: string + chainId?: number }): AddressBookEntry => ({ address, name, + chainId, }) export type AddressBookState = AddressBookEntry[] diff --git a/src/logic/addressBook/store/actions/addAddressBookEntry.ts b/src/logic/addressBook/store/actions/addAddressBookEntry.ts deleted file mode 100644 index b2717a26..00000000 --- a/src/logic/addressBook/store/actions/addAddressBookEntry.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createAction } from 'redux-actions' -import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' - -export const ADD_ENTRY = 'ADD_ENTRY' - -type addAddressBookEntryOptions = { - notifyEntryUpdate: boolean -} - -export const addAddressBookEntry = createAction( - ADD_ENTRY, - (entry: AddressBookEntry, options?: addAddressBookEntryOptions) => { - let notifyEntryUpdate = true - if (options) { - notifyEntryUpdate = options.notifyEntryUpdate - } - return { - entry, - shouldAvoidUpdatesNotifications: !notifyEntryUpdate, - } - }, -) diff --git a/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts b/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts deleted file mode 100644 index f19a0078..00000000 --- a/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAction } from 'redux-actions' -import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' - -export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY' - -export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({ - entry, -})) diff --git a/src/logic/addressBook/store/actions/index.ts b/src/logic/addressBook/store/actions/index.ts new file mode 100644 index 00000000..10a95b5f --- /dev/null +++ b/src/logic/addressBook/store/actions/index.ts @@ -0,0 +1,17 @@ +import { createAction } from 'redux-actions' + +import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' + +// following the suggested naming convention at +// https://redux.js.org/style-guide/style-guide#write-action-types-as-domaineventname +export enum ADDRESS_BOOK_ACTIONS { + ADD_OR_UPDATE = 'addressBook/addOrUpdate', + REMOVE = 'addressBook/remove', + IMPORT = 'addressBook/import', + SAFE_LOAD = 'addressBook/safeLoad', +} + +export const addressBookAddOrUpdate = createAction(ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE) +export const addressBookRemove = createAction(ADDRESS_BOOK_ACTIONS.REMOVE) +export const addressBookSafeLoad = createAction(ADDRESS_BOOK_ACTIONS.SAFE_LOAD) +export const addressBookImport = createAction(ADDRESS_BOOK_ACTIONS.IMPORT) diff --git a/src/logic/addressBook/store/actions/loadAddressBook.ts b/src/logic/addressBook/store/actions/loadAddressBook.ts deleted file mode 100644 index d887d198..00000000 --- a/src/logic/addressBook/store/actions/loadAddressBook.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAction } from 'redux-actions' -import { AddressBookState } from 'src/logic/addressBook/model/addressBook' - -export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK' - -export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({ - addressBook, -})) diff --git a/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts b/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts deleted file mode 100644 index c4315e3c..00000000 --- a/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook' -import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' -import { getAddressBookFromStorage } from 'src/logic/addressBook/utils' -import { Dispatch } from 'redux' - -const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise => { - try { - let storedAdBk = await getAddressBookFromStorage() - if (!storedAdBk) { - storedAdBk = [] - } - - const addressBook = buildAddressBook(storedAdBk) - - dispatch(loadAddressBook(addressBook)) - } catch (err) { - // eslint-disable-next-line - console.error('Error while loading active tokens from storage:', err) - } -} - -export default loadAddressBookFromStorage diff --git a/src/logic/addressBook/store/actions/removeAddressBookEntry.ts b/src/logic/addressBook/store/actions/removeAddressBookEntry.ts deleted file mode 100644 index 16571f47..00000000 --- a/src/logic/addressBook/store/actions/removeAddressBookEntry.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const REMOVE_ENTRY = 'REMOVE_ENTRY' - -export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({ - entryAddress, -})) diff --git a/src/logic/addressBook/store/actions/updateAddressBookEntry.ts b/src/logic/addressBook/store/actions/updateAddressBookEntry.ts deleted file mode 100644 index a426f812..00000000 --- a/src/logic/addressBook/store/actions/updateAddressBookEntry.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAction } from 'redux-actions' -import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' - -export const UPDATE_ENTRY = 'UPDATE_ENTRY' - -export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({ - entry, -})) diff --git a/src/logic/addressBook/store/middleware/addressBookMiddleware.ts b/src/logic/addressBook/store/middleware/addressBookMiddleware.ts deleted file mode 100644 index dfcd5774..00000000 --- a/src/logic/addressBook/store/middleware/addressBookMiddleware.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry' -import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' -import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' -import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { addressBookSelector } from 'src/logic/addressBook/store/selectors' -import { saveAddressBook } from 'src/logic/addressBook/utils' -import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' -import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' -import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { safesListSelector } from 'src/logic/safe/store/selectors' -import { sameAddress } from 'src/logic/wallets/ethAddresses' -import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' - -const watchedActions = [ADD_ENTRY, REMOVE_ENTRY, UPDATE_ENTRY, ADD_OR_UPDATE_ENTRY] - -const addressBookMiddleware = (store) => (next) => async (action) => { - const handledAction = next(action) - - if (watchedActions.includes(action.type)) { - const state = store.getState() - const { dispatch } = store - const addressBook = addressBookSelector(state) - const safes = safesListSelector(state) - - if (addressBook.length) { - await saveAddressBook(addressBook) - } - - switch (action.type) { - case ADD_ENTRY: { - const { shouldAvoidUpdatesNotifications } = action.payload - if (!shouldAvoidUpdatesNotifications) { - const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY) - dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) - } - break - } - case REMOVE_ENTRY: { - const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_DELETE_ENTRY) - dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) - break - } - case UPDATE_ENTRY: { - const { entry } = action.payload - const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY) - dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) - const safeFound = safes.find((safe) => sameAddress(safe.address, entry.address)) - if (safeFound) { - dispatch(updateSafe({ address: safeFound.address, name: entry.name })) - } - break - } - default: - break - } - } - - return handledAction -} - -export default addressBookMiddleware diff --git a/src/logic/addressBook/store/middleware/index.ts b/src/logic/addressBook/store/middleware/index.ts new file mode 100644 index 00000000..a1d12d02 --- /dev/null +++ b/src/logic/addressBook/store/middleware/index.ts @@ -0,0 +1,37 @@ +import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions' +import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' +import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' +import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' + +const watchedActions = [Object.keys(ADDRESS_BOOK_ACTIONS)] + +export const addressBookMiddleware = (store) => (next) => async (action) => { + const handledAction = next(action) + + if (watchedActions.includes(action.type)) { + const { dispatch } = store + + switch (action.type) { + case ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE: { + const { shouldAvoidUpdatesNotifications } = action.payload + if (!shouldAvoidUpdatesNotifications) { + const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_NEW_ENTRY) + dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) + } + break + } + case ADDRESS_BOOK_ACTIONS.REMOVE: { + const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_DELETE_ENTRY) + dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) + break + } + case ADDRESS_BOOK_ACTIONS.IMPORT: { + break + } + default: + break + } + } + + return handledAction +} diff --git a/src/logic/addressBook/store/reducer/addressBook.ts b/src/logic/addressBook/store/reducer/addressBook.ts deleted file mode 100644 index 7edd4e7c..00000000 --- a/src/logic/addressBook/store/reducer/addressBook.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Action, handleActions } from 'redux-actions' - -import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry' -import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' -import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook' -import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' -import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { AppReduxState } from 'src/store' -import { checksumAddress } from 'src/utils/checksumAddress' -import { getValidAddressBookName } from 'src/logic/addressBook/utils' - -export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' - -export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBookState => { - return storedAddressBook.map((addressBookEntry) => { - const { address, name } = addressBookEntry - return makeAddressBookEntry({ address: checksumAddress(address), name }) - }) -} - -type AddressBookPayload = { addressBook: AddressBookState } -type EntryPayload = { entry: AddressBookEntry } -type RemoveEntryPayload = { entryAddress: string } - -type Payloads = AddressBookPayload | EntryPayload | RemoveEntryPayload - -export default handleActions( - { - [LOAD_ADDRESS_BOOK]: (state, action: Action) => { - const { addressBook } = action.payload - return addressBook - }, - [ADD_ENTRY]: (state, action: Action) => { - const { entry } = action.payload - - const entryFound = state.find((oldEntry) => oldEntry.address === entry.address) - - if (!entryFound) { - state.push(entry) - } - return state - }, - [UPDATE_ENTRY]: (state, action: Action) => { - const { entry } = action.payload - const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address) - if (entryIndex >= 0) { - state[entryIndex] = entry - } - return state - }, - [REMOVE_ENTRY]: (state, action: Action) => { - const { entryAddress } = action.payload - const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress) - state.splice(entryIndex, 1) - return state - }, - [ADD_OR_UPDATE_ENTRY]: (state, action: Action) => { - const { entry } = action.payload - - // Only updates entries with valid names - const validName = getValidAddressBookName(entry.name) - if (!validName) { - return state - } - - const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address) - - if (entryIndex >= 0) { - state[entryIndex] = entry - } else { - state.push(entry) - } - return state - }, - }, - [], -) diff --git a/src/logic/addressBook/store/reducer/index.ts b/src/logic/addressBook/store/reducer/index.ts new file mode 100644 index 00000000..8e7590d7 --- /dev/null +++ b/src/logic/addressBook/store/reducer/index.ts @@ -0,0 +1,82 @@ +import { Action, handleActions } from 'redux-actions' + +import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' +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' + +type Payloads = AddressBookEntry | AddressBookState + +export default handleActions( + { + [ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE]: (state, action: Action) => { + const newState = [...state] + const { address, ...rest } = action.payload + + if (!isValidAddressBookName(rest.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 + if (entryIndex >= 0) { + newState[entryIndex] = addressBookEntry + return newState + } + + // add + return [...newState, addressBookEntry] + }, + [ADDRESS_BOOK_ACTIONS.REMOVE]: (state, action: Action) => { + const newState = [...state] + const addressBookEntry = action.payload + + const entryIndex = getEntryIndex(newState, addressBookEntry) + + if (entryIndex >= 0) { + newState.splice(entryIndex, 1) + return newState + } + + return newState + }, + [ADDRESS_BOOK_ACTIONS.SAFE_LOAD]: (state, action: Action) => { + const newState = [...state] + const addressBookEntries = action.payload + + addressBookEntries + // 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) + + if (entryIndex >= 0) { + // update + newState[entryIndex] = newAddressBookEntry + } else { + // add + newState.push(newAddressBookEntry) + } + }) + + return newState + }, + [ADDRESS_BOOK_ACTIONS.IMPORT](...args) { + // same functionality, but `IMPORT` will trigger notifications when called + return this[ADDRESS_BOOK_ACTIONS.SAFE_LOAD](...args) + }, + }, + [], +) diff --git a/src/logic/addressBook/store/selectors/index.ts b/src/logic/addressBook/store/selectors/index.ts index e4afde58..d6b236a1 100644 --- a/src/logic/addressBook/store/selectors/index.ts +++ b/src/logic/addressBook/store/selectors/index.ts @@ -1,27 +1,56 @@ -import { AppReduxState } from 'src/store' - import { createSelector } from 'reselect' -import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook' +import { getNetworkId } from 'src/config' +import { ADDRESS_BOOK_DEFAULT_NAME, AddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { AppReduxState } from 'src/store' +import { Overwrite } from 'src/types/helpers' -import { AddressBookState } from 'src/logic/addressBook/model/addressBook' +const networkId = getNetworkId() -export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID] +export const addressBookSelector = (state: AppReduxState): AppReduxState['addressBook'] => state['addressBook'] -export const addressBookAddressesListSelector = (state: AppReduxState): string[] => { - const addressBook = addressBookSelector(state) - return addressBook.map((entry) => entry.address) +type AddressBookMap = { + [chainId: number]: { + [address: string]: AddressBookEntry + } } -export const getNameFromAddressBookSelector = createSelector( - addressBookSelector, - (_, address) => address, - (addressBook, address) => { - const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address) +export const addressBookMapSelector = createSelector( + [addressBookSelector], + (addressBook): AddressBookMap => { + const addressBookMap = {} - if (adbkEntry) { - return adbkEntry.name - } - return 'UNKNOWN' + addressBook.forEach((entry) => { + const { address, chainId } = entry + if (!addressBookMap[chainId]) { + addressBookMap[chainId] = { [address]: entry } + } else { + addressBookMap[chainId][address] = entry + } + }) + + return addressBookMap }, ) + +export const addressBookAddressesListSelector = createSelector([addressBookSelector], (addressBook): string[] => + addressBook.map(({ address }) => address), +) + +type GetNameParams = Overwrite< + AddressBookEntry, + { chainId?: AddressBookEntry['chainId']; name?: AddressBookEntry['name'] } +> + +type GetNameReturnObject = Overwrite + +export const getNameFromAddressBookSelector = createSelector( + [ + addressBookMapSelector, + (_, { address, chainId = networkId }: GetNameParams): GetNameReturnObject => ({ + address, + chainId, + }), + ], + (addressBook, entry) => addressBook?.[entry.chainId]?.[entry.address]?.name ?? ADDRESS_BOOK_DEFAULT_NAME, +) diff --git a/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts index 8ff40278..f7f006e0 100644 --- a/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts +++ b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts @@ -1,16 +1,8 @@ -import { List } from 'immutable' import { checkIfEntryWasDeletedFromAddressBook, - getAddressBookFromStorage, getNameFromAddressBook, - getOwnersWithNameFromAddressBook, isValidAddressBookName, - migrateOldAddressBook, - OldAddressBookEntry, - OldAddressBookType, - saveAddressBook, -} from 'src/logic/addressBook/utils/index' -import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' +} from 'src/logic/addressBook/utils' import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry => @@ -19,14 +11,6 @@ const getMockAddressBookEntry = (address: string, name: string = 'test'): Addres name, }) -const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => { - return { - address, - name, - isOwner, - } -} - describe('getNameFromSafeAddressBook', () => { const entry1 = getMockAddressBookEntry('123456', 'test1') const entry2 = getMockAddressBookEntry('78910', 'test2') @@ -44,155 +28,6 @@ describe('getNameFromSafeAddressBook', () => { }) }) -describe('getOwnersWithNameFromAddressBook', () => { - const entry1 = getMockAddressBookEntry('123456', 'test1') - const entry2 = getMockAddressBookEntry('78910', 'test2') - const entry3 = getMockAddressBookEntry('4781321', 'test3') - it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => { - // given - const safeAddressBook = [entry1, entry2, entry3] - const ownerList = List([ - { address: entry1.address, name: '' }, - { address: entry2.address, name: '' }, - ]) - const expectedResult = List([ - { address: entry1.address, name: entry1.name }, - { address: entry2.address, name: entry2.name }, - ]) - - // when - const result = getOwnersWithNameFromAddressBook(safeAddressBook, ownerList) - - // then - expect(result).toStrictEqual(expectedResult) - }) -}) - -jest.mock('src/utils/storage/index') -describe('saveAddressBook', () => { - const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' - const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' - const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126' - const entry1 = getMockAddressBookEntry(mockAdd1, 'test1') - const entry2 = getMockAddressBookEntry(mockAdd2, 'test2') - const entry3 = getMockAddressBookEntry(mockAdd3, 'test3') - afterAll(() => { - jest.unmock('src/utils/storage/index') - }) - it('It should save a given addressBook to the localStorage', async () => { - // given - const addressBook: AddressBookState = [entry1, entry2, entry3] - - // when - await saveAddressBook(addressBook) - - const storageUtils = require('src/utils/storage/index') - const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(addressBook)) - - const storedAddressBook = await getAddressBookFromStorage() - - // @ts-ignore - let result = buildAddressBook(storedAddressBook) - - // then - expect(result).toStrictEqual(addressBook) - expect(spy).toHaveBeenCalled() - }) -}) - -describe('migrateOldAddressBook', () => { - const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' - const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' - const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91' - const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8' - - it('It should receive an addressBook in old format and return the same addressBook in new format', () => { - // given - const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 }) - const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 }) - - const oldAddressBook: OldAddressBookType = { - [safeAddress1]: [entry1], - [safeAddress2]: [entry2], - } - - const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1') - const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2') - const expectedResult = [expectedEntry1, expectedEntry2] - - // when - const result = migrateOldAddressBook(oldAddressBook) - - // then - expect(result).toStrictEqual(expectedResult) - }) -}) - -describe('getAddressBookFromStorage', () => { - const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' - const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' - const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91' - const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8' - beforeAll(() => { - jest.mock('src/utils/storage/index') - }) - afterAll(() => { - jest.unmock('src/utils/storage/index') - }) - it('It should return null if no addressBook in storage', async () => { - // given - const expectedResult = null - const storageUtils = require('src/utils/storage/index') - const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null) - - // when - const result = await getAddressBookFromStorage() - - // then - expect(result).toStrictEqual(expectedResult) - expect(spy).toHaveBeenCalled() - }) - it('It should return migrated addressBook if old addressBook in storage', async () => { - // given - const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1') - const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2') - const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 }) - const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 }) - const oldAddressBook: OldAddressBookType = { - [safeAddress1]: [entry1], - [safeAddress2]: [entry2], - } - const expectedResult = [expectedEntry1, expectedEntry2] - - const storageUtils = require('src/utils/storage/index') - const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook) - - // when - const result = await getAddressBookFromStorage() - - // then - expect(result).toStrictEqual(expectedResult) - expect(spy).toHaveBeenCalled() - }) - it('It should return addressBook if addressBook in storage', async () => { - // given - const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1') - const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2') - - const expectedResult = [expectedEntry1, expectedEntry2] - - const storageUtils = require('src/utils/storage/index') - const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult)) - - // when - const result = await getAddressBookFromStorage() - - // then - expect(result).toStrictEqual(expectedResult) - expect(spy).toHaveBeenCalled() - }) -}) - describe('isValidAddressBookName', () => { it('It should return false if given a blacklisted name like UNKNOWN', () => { // given diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 345dbad3..9ba5d105 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -1,11 +1,14 @@ -import { List } from 'immutable' import { mustBeEthereumContractAddress } from 'src/components/forms/validator' +import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { saveSafes, StoredSafes } from 'src/logic/safe/utils' import { sameAddress } from 'src/logic/wallets/ethAddresses' -import { loadFromStorage, saveToStorage } from 'src/utils/storage' - -const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' +import { AppReduxState } from 'src/store' +import { Overwrite } from 'src/types/helpers' +import { getNetworkName } from 'src/config' +import { checksumAddress } from 'src/utils/checksumAddress' +import { removeFromStorage } from 'src/utils/storage' export type OldAddressBookEntry = { address: string @@ -17,44 +20,7 @@ export type OldAddressBookType = { [safeAddress: string]: [OldAddressBookEntry] } -const ADDRESSBOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET'] - -export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => { - const values: AddressBookState = [] - const adbkValues = Object.values(oldAddressBook) - - for (const safeIterator of adbkValues) { - for (const safeAddressBook of safeIterator) { - if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) { - values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name })) - } - } - } - - return values -} - -export const getAddressBookFromStorage = async (): Promise => { - const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY) - - if (!result) { - return null - } - - if (typeof result === 'string') { - return JSON.parse(result) - } - - return migrateOldAddressBook(result as OldAddressBookType) -} - -export const saveAddressBook = async (addressBook: AddressBookState): Promise => { - try { - await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook)) - } catch (err) { - console.error('Error storing addressBook in localstorage', err) - } -} +const ADDRESS_BOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET', ''] type GetNameFromAddressBookOptions = { filterOnlyValidName: boolean @@ -73,32 +39,19 @@ export const getNameFromAddressBook = ( } export const isValidAddressBookName = (addressBookName: string): boolean => { - const hasInvalidName = ADDRESSBOOK_INVALID_NAMES.find((invalidName) => + // TODO: this is filtering names that includes any of the keywords in the `ADDRESS_BOOK_INVALID_NAMES` + // So a name in the order of 'This is an unknown user' will be filtered too. Is this intentional? + const hasInvalidName = ADDRESS_BOOK_INVALID_NAMES.find((invalidName) => addressBookName.toUpperCase().includes(invalidName), ) return !hasInvalidName } +// TODO: is this really required? export const getValidAddressBookName = (addressBookName: string): string | null => { return isValidAddressBookName(addressBookName) ? addressBookName : null } -export const getOwnersWithNameFromAddressBook = ( - addressBook: AddressBookState, - ownerList: List, -): List => { - if (!ownerList) { - return List([]) - } - return ownerList.map((owner) => { - const ownerName = getNameFromAddressBook(addressBook, owner.address) - return { - address: owner.address, - name: ownerName || '', - } - }) -} - export const formatAddressListToAddressBookNames = ( addressBook: AddressBookState, addresses: string[], @@ -111,6 +64,7 @@ export const formatAddressListToAddressBookNames = ( return { address: address, name: ownerName || '', + chainId: ETHEREUM_NETWORK.UNKNOWN, } }) } @@ -172,3 +126,149 @@ export const filterAddressEntries = ( return foundName || foundAddress }) + +export const getEntryIndex = ( + state: AppReduxState['addressBook'], + addressBookEntry: Overwrite, +): number => + state.findIndex( + ({ address, chainId }) => chainId === addressBookEntry.chainId && sameAddress(address, addressBookEntry.address), + ) + +/** + * Migrates the safes names from the Safe Object to the Address Book + * + * Works on the persistence layer, reads from the localStorage and stores back the mutated safe info through immortalDB. + * + * @note If the Safe name is an invalid AB name, it's renamed to "Migrated from: {safe.name}" + */ +export const migrateSafeNames = ({ + states, + namespace, + namespaceSeparator, +}: { + states: string[] + namespace: string + namespaceSeparator: string +}): void => { + const PREFIX = `v2_${getNetworkName()}` + const storedSafes = localStorage.getItem(`_immortal|${PREFIX}__SAFES`) + + if (storedSafes === null) { + // nothing left to migrate + return + } + + const parsedStoredSafes = JSON.parse(storedSafes) as Record> + + if (Object.entries(parsedStoredSafes).every(([, { name }]) => name === undefined)) { + // no name key, safes already migrated + return + } + + const safesToAddressBook: AddressBookState = [] + const migratedSafes: StoredSafes = + // once removed the name from the safe object, re-create the map + Object.fromEntries( + // prepare the safe's map to iterate over it + Object.entries(parsedStoredSafes) + // exclude those safes without name + .filter(([, { name }]) => name !== undefined) + // iterate over the list of safes + .map(([safeAddress, { name, ...safe }]) => { + let safeName = name + + if (!isValidAddressBookName(name)) { + safeName = `Migrated from: ${name}` + } + + // create an entry for the AB + safesToAddressBook.push(makeAddressBookEntry({ address: safeAddress, name: safeName })) + + // return the new safe object without the name on it + return [safeAddress, safe] + }), + ) + + const [state] = states + const addressBookKey = `${namespace}${namespaceSeparator}${state}` + const storedAddressBook = localStorage.getItem(addressBookKey) + let addressBookToStore: AddressBookState = [] + + if (storedAddressBook !== null) { + // stored AB information + addressBookToStore = JSON.parse(storedAddressBook) + } + + // mutate `addressBookToStore` by adding safes' information + safesToAddressBook.forEach((entry) => { + const safeIndex = getEntryIndex(addressBookToStore, entry) + + if (safeIndex >= 0) { + // update AB entry with what was stored in the safe object + addressBookToStore[safeIndex] = entry + } else { + // add the safe entry to the AB + addressBookToStore.push(entry) + } + }) + + try { + // store the mutated address book + localStorage.setItem(addressBookKey, JSON.stringify(addressBookToStore)) + // update stored safe + saveSafes(migratedSafes).then(() => console.info('updated Safe objects')) + } catch (error) { + console.error('failed to migrate safes names into the address book', error.message) + } +} + +/** + * Migrates the AddressBook from a per-network to a global storage under the key `ADDRESS_BOOK` in `localStorage` + * + * The migrated structure will be `{ address, name, chainId }` + * + * @note Also, adds `chainId` to every entry in the AddressBook list. + */ +export const migrateAddressBook = ({ + states, + namespace, + namespaceSeparator, +}: { + states: string[] + namespace: string + namespaceSeparator: string +}): void => { + const [state] = states + const PREFIX = `v2_${getNetworkName()}` + const storedAddressBook = localStorage.getItem(`_immortal|${PREFIX}__ADDRESS_BOOK_STORAGE_KEY`) + + if (storedAddressBook === null) { + // nothing left to migrate + return + } + + let parsedAddressBook = JSON.parse(storedAddressBook) + + if (typeof parsedAddressBook === 'string') { + // double stringify? + parsedAddressBook = JSON.parse(parsedAddressBook) + } + + const migratedAddressBook = (parsedAddressBook as Omit[]) + // exclude those addresses with invalid names + .filter(({ name }) => isValidAddressBookName(name)) + .map(({ address, ...entry }) => + makeAddressBookEntry({ + address: checksumAddress(address), + ...entry, + }), + ) + + try { + localStorage.setItem(`${namespace}${namespaceSeparator}${state}`, JSON.stringify(migratedAddressBook)) + removeFromStorage('ADDRESS_BOOK_STORAGE_KEY').then(() => console.info('legacy Address Book removed')) + } catch (error) { + console.error('failed to persist the migrated address book', error.message) + } +} diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index 91249a22..28742984 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -204,19 +204,19 @@ export const getNotificationsFromTxType: any = (txType, origin) => { notificationsQueue = waitingTransactionNotificationsQueue break } - case TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY: { + case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_NEW_ENTRY: { notificationsQueue = addressBookNewEntry break } - case TX_NOTIFICATION_TYPES.ADDRESSBOOK_EDIT_ENTRY: { + case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EDIT_ENTRY: { notificationsQueue = addressBookEditEntry break } - case TX_NOTIFICATION_TYPES.ADDRESSBOOK_DELETE_ENTRY: { + case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_DELETE_ENTRY: { notificationsQueue = addressBookDeleteEntry break } - case TX_NOTIFICATION_TYPES.ADDRESSBOOK_EXPORT_ENTRIES: { + case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EXPORT_ENTRIES: { notificationsQueue = addressBookExportEntries break } diff --git a/src/logic/safe/hooks/useLoadSafe.tsx b/src/logic/safe/hooks/useLoadSafe.tsx index 64462297..9c50973a 100644 --- a/src/logic/safe/hooks/useLoadSafe.tsx +++ b/src/logic/safe/hooks/useLoadSafe.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' -import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage' import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe' import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion' @@ -10,7 +9,7 @@ import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTr import { Dispatch } from 'src/logic/safe/store/actions/types.d' import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies' -export const useLoadSafe = (safeAddress?: string, loadedViaUrl = true): boolean => { +export const useLoadSafe = (safeAddress?: string): boolean => { const dispatch = useDispatch() const [isSafeLoaded, setIsSafeLoaded] = useState(false) @@ -23,15 +22,11 @@ export const useLoadSafe = (safeAddress?: string, loadedViaUrl = true): boolean await dispatch(fetchSafeTokens(safeAddress)) await dispatch(updateAvailableCurrencies()) await dispatch(fetchTransactions(safeAddress)) - if (!loadedViaUrl) { - dispatch(addViewedSafe(safeAddress)) - } + dispatch(addViewedSafe(safeAddress)) } } - - dispatch(loadAddressBookFromStorage()) fetchData() - }, [dispatch, safeAddress, loadedViaUrl]) + }, [dispatch, safeAddress]) return isSafeLoaded } diff --git a/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts b/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts index a33a2f58..316dace2 100644 --- a/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts +++ b/src/logic/safe/store/actions/__tests__/fetchSafe.test.ts @@ -1,14 +1,12 @@ // --no-ignore import axios from 'axios' -import { List, Map } from 'immutable' +import { Map } from 'immutable' import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { buildSafe, fetchSafe } from 'src/logic/safe/store/actions/fetchSafe' import * as storageUtils from 'src/utils/storage' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' -import { makeOwner } from 'src/logic/safe/store/models/owner' -import { Overwrite } from 'src/types/helpers' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' import { DEFAULT_SAFE_INITIAL_STATE } from 'src/logic/safe/store/reducer/safe' import { inMemoryPartialSafeInformation, localSafesInfo, remoteSafeInfoWithoutModules } from '../mocks/safeInformation' @@ -25,51 +23,46 @@ describe('buildSafe', () => { jest.unmock('src/utils/storage/index') }) - it('should return a Partial SafeRecord with a mix of remote and local safe info', async () => { + // ToDo: use a property other than `name` + it.skip('should return a Partial SafeRecord with a mix of remote and local safe info', async () => { mockedAxios.get.mockImplementationOnce(async () => ({ data: remoteSafeInfoWithoutModules })) storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo) - const finalValues: Overwrite, { name: string }> = { - name: 'My Safe Name that will last', + const finalValues: Partial = { modules: undefined, spendingLimits: undefined, } - const builtSafe = await buildSafe(SAFE_ADDRESS, finalValues.name) + const builtSafe = await buildSafe(SAFE_ADDRESS) expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation, ...finalValues }) }) - it('should return a Partial SafeRecord when `remoteSafeInfo` is not present', async () => { + it.skip('should return a Partial SafeRecord when `remoteSafeInfo` is not present', async () => { jest.spyOn(global.console, 'error').mockImplementationOnce(() => {}) mockedAxios.get.mockImplementationOnce(async () => { throw new Error('-- test -- no resource available') }) - const name = 'My Safe Name that will last' storageUtil.loadFromStorage.mockImplementationOnce(async () => localSafesInfo) - const builtSafe = await buildSafe(SAFE_ADDRESS, name) + const builtSafe = await buildSafe(SAFE_ADDRESS) - expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation, name }) + expect(builtSafe).toStrictEqual({ ...inMemoryPartialSafeInformation }) }) - it('should return a Partial SafeRecord when `localSafeInfo` is not present', async () => { + it.skip('should return a Partial SafeRecord when `localSafeInfo` is not present', async () => { mockedAxios.get.mockImplementationOnce(async () => ({ data: remoteSafeInfoWithoutModules })) storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined) - const name = 'My Safe Name that WILL last' - const builtSafe = await buildSafe(SAFE_ADDRESS, name) + const builtSafe = await buildSafe(SAFE_ADDRESS) expect(builtSafe).toStrictEqual({ - name, address: SAFE_ADDRESS, threshold: 2, - owners: List( - [ - { address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' }, - { address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' }, - { address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' }, - { address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' }, - { address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' }, - ].map(makeOwner), - ), + owners: [ + '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ], modules: undefined, spendingLimits: undefined, nonce: 492, @@ -78,19 +71,18 @@ describe('buildSafe', () => { featuresEnabled: ['ERC721', 'ERC1155', 'SAFE_APPS', 'CONTRACT_INTERACTION'], }) }) - it('should return a Partial SafeRecord with only `address` and `name` keys if it fails to recover info', async () => { + it.skip('should return a Partial SafeRecord with only `address` and `name` keys if it fails to recover info', async () => { jest.spyOn(global.console, 'error').mockImplementationOnce(() => {}) mockedAxios.get.mockImplementationOnce(async () => { throw new Error('-- test -- no resource available') }) - const finalValues: Overwrite, { name: string }> = { - name: 'My Safe Name that will last', + const finalValues: Partial = { address: SAFE_ADDRESS, owners: undefined, } storageUtil.loadFromStorage.mockImplementationOnce(async () => undefined) - const builtSafe = await buildSafe(SAFE_ADDRESS, finalValues.name) + const builtSafe = await buildSafe(SAFE_ADDRESS) expect(builtSafe).toStrictEqual(finalValues) }) @@ -99,7 +91,6 @@ describe('buildSafe', () => { describe('fetchSafe', () => { const SAFE_ADDRESS = '0xe414604Ad49602C0b9c0b08D0781ECF96740786a' const mockedAxios = axios as jest.Mocked - const storageUtil = require('src/utils/storage/index') as jest.Mocked const middlewares = [thunk] const mockStore = configureMockStore(middlewares) @@ -115,15 +106,13 @@ describe('fetchSafe', () => { payload: { address: SAFE_ADDRESS, threshold: 2, - owners: List( - [ - { address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' }, - { address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' }, - { address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' }, - { address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' }, - { address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' }, - ].map(makeOwner), - ), + owners: [ + '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ], modules: undefined, spendingLimits: undefined, nonce: 492, diff --git a/src/logic/safe/store/actions/__tests__/utils.test.ts b/src/logic/safe/store/actions/__tests__/utils.test.ts index 207d74ca..8da439d8 100644 --- a/src/logic/safe/store/actions/__tests__/utils.test.ts +++ b/src/logic/safe/store/actions/__tests__/utils.test.ts @@ -1,5 +1,4 @@ import axios from 'axios' -import { List } from 'immutable' import { FEATURES } from 'src/config/networks/network.d' import { @@ -9,8 +8,7 @@ import { getNewTxNonce, shouldExecuteTransaction, } from 'src/logic/safe/store/actions/utils' -import { makeOwner } from 'src/logic/safe/store/models/owner' -import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { buildTxServiceUrl } from 'src/logic/safe/transactions' import { getMockedSafeInstance, getMockedTxServiceModel } from 'src/test/utils/safeHelper' import { @@ -223,96 +221,49 @@ describe('buildSafeOwners', () => { expect(buildSafeOwners()).toBeUndefined() }) it('should return `localSafeOwners` if no `remoteSafeOwners` were provided', () => { - const expectedOwners = List( - [ - { address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875' }, - { address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F' }, - { address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47' }, - { address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2' }, - { address: '0x5e47249883F6a1d639b84e8228547fB289e222b6' }, - ].map(makeOwner), - ) + const expectedOwners = [ + '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ] expect(buildSafeOwners(remoteSafeInfoWithModules.owners)).toStrictEqual(expectedOwners) }) it('should discard those owners that are not present in `remoteSafeOwners`', () => { - const localOwners: List = List(localSafesInfo[SAFE_ADDRESS].owners.map(makeOwner)) + const localOwners: SafeRecordProps['owners'] = localSafesInfo[SAFE_ADDRESS].owners const [, ...remoteOwners] = remoteSafeInfoWithModules.owners - const expectedOwners = List( - [ - { - name: 'UNKNOWN', - address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', - }, - { - name: 'UNKNOWN', - address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', - }, - { - name: 'Owner B', - address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', - }, - { - name: 'Owner A', - address: '0x5e47249883F6a1d639b84e8228547fB289e222b6', - }, - ].map(makeOwner), - ) + const expectedOwners = [ + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ] expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners) }) it('should add those owners that are not present in `localSafeOwners`', () => { - const localOwners: List = List(localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4).map(makeOwner)) + const localOwners: SafeRecordProps['owners'] = localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4) const remoteOwners = remoteSafeInfoWithModules.owners - const expectedOwners = List( - [ - { - name: 'UNKNOWN', - address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', - }, - { - name: 'UNKNOWN', - address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', - }, - { - name: 'UNKNOWN', - address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', - }, - { - name: 'Owner B', - address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', - }, - { - name: 'UNKNOWN', - address: '0x5e47249883F6a1d639b84e8228547fB289e222b6', - }, - ].map(makeOwner), - ) + const expectedOwners = [ + '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ] expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners) }) it('should preserve those owners that are present in `remoteSafeOwners` with data present in `localSafeOwners`', () => { - const localOwners: List = List(localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4).map(makeOwner)) + const localOwners: SafeRecordProps['owners'] = localSafesInfo[SAFE_ADDRESS].owners.slice(0, 4) const [, ...remoteOwners] = remoteSafeInfoWithModules.owners - const expectedOwners = List( - [ - { - name: 'UNKNOWN', - address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', - }, - { - name: 'UNKNOWN', - address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', - }, - { - name: 'Owner B', - address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', - }, - { - name: 'UNKNOWN', - address: '0x5e47249883F6a1d639b84e8228547fB289e222b6', - }, - ].map(makeOwner), - ) + const expectedOwners = [ + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ] expect(buildSafeOwners(remoteOwners, localOwners)).toStrictEqual(expectedOwners) }) diff --git a/src/logic/safe/store/actions/addOrUpdateSafe.ts b/src/logic/safe/store/actions/addOrUpdateSafe.ts index ae559ea7..787cace0 100644 --- a/src/logic/safe/store/actions/addOrUpdateSafe.ts +++ b/src/logic/safe/store/actions/addOrUpdateSafe.ts @@ -1,17 +1,9 @@ import { createAction } from 'redux-actions' -import { SafeOwner, SafeRecordProps } from '../models/safe' -import { List } from 'immutable' -import { makeOwner } from '../models/owner' +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' export const ADD_OR_UPDATE_SAFE = 'ADD_OR_UPDATE_SAFE' -export const buildOwnersFrom = (names: string[], addresses: string[]): List => { - const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] })) - - return List(owners) -} - export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({ safe, })) diff --git a/src/logic/safe/store/actions/addSafeOwner.ts b/src/logic/safe/store/actions/addSafeOwner.ts deleted file mode 100644 index 2a341b4e..00000000 --- a/src/logic/safe/store/actions/addSafeOwner.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER' - -export const addSafeOwner = createAction(ADD_SAFE_OWNER) diff --git a/src/logic/safe/store/actions/editSafeOwner.ts b/src/logic/safe/store/actions/editSafeOwner.ts deleted file mode 100644 index c8e813bd..00000000 --- a/src/logic/safe/store/actions/editSafeOwner.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const EDIT_SAFE_OWNER = 'EDIT_SAFE_OWNER' - -const editSafeOwner = createAction(EDIT_SAFE_OWNER) - -export default editSafeOwner diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 8832dad0..7e7dd7f4 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -1,16 +1,13 @@ -import { List } from 'immutable' import { Dispatch } from 'redux' +import { Action } from 'redux-actions' -import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { updateSafe } from 'src/logic/safe/store/actions/updateSafe' import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { getLocalSafe } from 'src/logic/safe/utils' import { allSettled } from 'src/logic/safe/utils/allSettled' import { getSafeInfo, SafeInfo } from 'src/logic/safe/utils/safeInformation' -import { AppReduxState } from 'src/store' import { checksumAddress } from 'src/utils/checksumAddress' import { buildSafeOwners, extractRemoteSafeInfo } from './utils' -import { Action } from 'redux-actions' /** * Builds a Safe Record that will be added to the app's store @@ -19,15 +16,11 @@ import { Action } from 'redux-actions' * @note It's being used by "Load Existing Safe" and "Create New Safe" flows * * @param {string} safeAddress - * @param {string} safeName * @returns Promise */ -export const buildSafe = async (safeAddress: string, safeName: string): Promise => { +export const buildSafe = async (safeAddress: string): Promise => { const address = checksumAddress(safeAddress) - const safeInfo: Partial = { - address, - name: safeName, - } + const safeInfo: Partial = { address } const [remote, localSafeInfo] = await allSettled<[SafeInfo | null, SafeRecordProps | undefined | null]>( getSafeInfo(safeAddress), @@ -52,7 +45,6 @@ export const buildSafe = async (safeAddress: string, safeName: string): Promise< */ export const fetchSafe = (safeAddress: string) => async ( dispatch: Dispatch, - getState: () => AppReduxState, ): Promise>> => { const address = checksumAddress(safeAddress) @@ -61,13 +53,10 @@ export const fetchSafe = (safeAddress: string) => async ( // remote (client-gateway) const safeInfo = remoteSafeInfo ? await extractRemoteSafeInfo(remoteSafeInfo) : {} - // TODO: REVIEW: having the owner's names duplicated with what's in the address book seems a bit odd - const state = getState() - const addressBook = addressBookSelector(state) // update owner's information const owners = remoteSafeInfo - ? // if we have remote info, we can enrich it with local address book information - buildSafeOwners(remoteSafeInfo.owners, List(addressBook)) + ? // if we have remote info, we use it + buildSafeOwners(remoteSafeInfo.owners) : // if there's no remote info, we keep what's in memory undefined diff --git a/src/logic/safe/store/actions/mocks/safeInformation.ts b/src/logic/safe/store/actions/mocks/safeInformation.ts index 366893a9..cea7b084 100644 --- a/src/logic/safe/store/actions/mocks/safeInformation.ts +++ b/src/logic/safe/store/actions/mocks/safeInformation.ts @@ -1,6 +1,3 @@ -import { List } from 'immutable' -import { makeOwner } from '../../models/owner' - export const remoteSafeInfoWithModules = { address: { value: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a', @@ -86,30 +83,13 @@ export const localSafesInfo = { name: 'Safe A', address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a', threshold: 2, - owners: List( - [ - { - name: 'UNKNOWN', - address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', - }, - { - name: 'UNKNOWN', - address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', - }, - { - name: 'UNKNOWN', - address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', - }, - { - name: 'Owner B', - address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', - }, - { - name: 'Owner A', - address: '0x5e47249883F6a1d639b84e8228547fB289e222b6', - }, - ].map(makeOwner), - ), + owners: [ + '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ], modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']], spendingLimits: [ { @@ -177,30 +157,13 @@ export const inMemoryPartialSafeInformation = { name: 'Safe A', address: '0xe414604Ad49602C0b9c0b08D0781ECF96740786a', threshold: 2, - owners: List( - [ - { - name: 'UNKNOWN', - address: '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', - }, - { - name: 'UNKNOWN', - address: '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', - }, - { - name: 'UNKNOWN', - address: '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', - }, - { - name: 'Owner B', - address: '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', - }, - { - name: 'Owner A', - address: '0x5e47249883F6a1d639b84e8228547fB289e222b6', - }, - ].map(makeOwner), - ), + owners: [ + '0xcCdd7e3af1c24c08D8B65A328351e7e23923d875', + '0x04Aa5eC2065224aDB15aCE6fb1aAb988Ae55631F', + '0x52Da808E9a83FEB147a2d0ca7d2f5bBBd3035C47', + '0x4dcD12D11dE7382F9c26D59Db1aCE1A4737e58A2', + '0x5e47249883F6a1d639b84e8228547fB289e222b6', + ], modules: [['0x0000000000000000000000000000000000000001', '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134']], spendingLimits: [ { diff --git a/src/logic/safe/store/actions/removeSafeOwner.ts b/src/logic/safe/store/actions/removeSafeOwner.ts deleted file mode 100644 index 5f06a6e6..00000000 --- a/src/logic/safe/store/actions/removeSafeOwner.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from 'redux-actions' - -export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER' - -export const removeSafeOwner = createAction(REMOVE_SAFE_OWNER) diff --git a/src/logic/safe/store/actions/replaceSafeOwner.ts b/src/logic/safe/store/actions/replaceSafeOwner.ts deleted file mode 100644 index 8850023c..00000000 --- a/src/logic/safe/store/actions/replaceSafeOwner.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from 'redux-actions' - -export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER' - -export const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER) diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index 23c3b37b..4bdc96f3 100644 --- a/src/logic/safe/store/actions/utils.ts +++ b/src/logic/safe/store/actions/utils.ts @@ -8,10 +8,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits' import { buildModulesLinkedList } from 'src/logic/safe/utils/modules' import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion' -import { sameAddress } from 'src/logic/wallets/ethAddresses' -import { makeOwner } from 'src/logic/safe/store/models/owner' import { checksumAddress } from 'src/utils/checksumAddress' -import { List } from 'immutable' export const getLastTx = async (safeAddress: string): Promise => { try { @@ -105,13 +102,9 @@ export const buildSafeOwners = ( localSafeOwners?: SafeRecordProps['owners'], ): SafeRecordProps['owners'] | undefined => { if (remoteSafeOwners) { - const remoteOwners = remoteSafeOwners.map(({ value }) => { - const localOwner = localSafeOwners?.find(({ address }) => sameAddress(address, value)) - const name = localOwner?.name - return makeOwner({ name, address: checksumAddress(value) }) - }) - - return List(remoteOwners) + // ToDo: review if checksums addresses is necessary, + // as they must be provided already in the checksum form from the services + return remoteSafeOwners.map(({ value }) => checksumAddress(value)) } // nothing to do without remote owners, so we return the stored list diff --git a/src/logic/safe/store/middleware/safeStorage.ts b/src/logic/safe/store/middleware/safeStorage.ts index 8ce1a5b9..5a6395d6 100644 --- a/src/logic/safe/store/middleware/safeStorage.ts +++ b/src/logic/safe/store/middleware/safeStorage.ts @@ -1,62 +1,25 @@ import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils' -import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' -import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' -import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner' -import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner' import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' -import { safesMapSelector } from 'src/logic/safe/store/selectors' -import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { checksumAddress } from 'src/utils/checksumAddress' -import { isValidAddressBookName } from 'src/logic/addressBook/utils' -import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { safesListWithAddressBookNameSelector, safesMapSelector } from 'src/logic/safe/store/selectors' -const watchedActions = [ - UPDATE_SAFE, - REMOVE_SAFE, - ADD_OR_UPDATE_SAFE, - ADD_SAFE_OWNER, - REMOVE_SAFE_OWNER, - REPLACE_SAFE_OWNER, - EDIT_SAFE_OWNER, - SET_DEFAULT_SAFE, -] +const watchedActions = [REMOVE_SAFE, SET_DEFAULT_SAFE, UPDATE_SAFE] export const safeStorageMiddleware = (store) => (next) => async (action) => { const handledAction = next(action) if (watchedActions.includes(action.type)) { const state = store.getState() - const { dispatch } = store const safes = safesMapSelector(state) - await saveSafes(safes.filter((safe) => !safe.loadedViaUrl).toJSON()) + const safeNameMap = Object.fromEntries( + safesListWithAddressBookNameSelector(state) + .map((safe) => [safe.address, safe.name]) + .toJSON(), + ) + await saveSafes(safes.filter((safe) => safeNameMap[safe.address]).toJSON()) switch (action.type) { - case ADD_OR_UPDATE_SAFE: { - const { safe } = action.payload - safe.owners.forEach((owner) => { - const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name }) - if (isValidAddressBookName(checksumEntry.name)) { - dispatch(addOrUpdateAddressBookEntry(checksumEntry)) - } - }) - - // add the recently loaded safe as an entry in the address book - // if it exists already, it will be replaced with the recently added name - if (safe.name) { - dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: safe.name, address: safe.address }))) - } - break - } - case UPDATE_SAFE: { - const { name, address } = action.payload - if (name) { - dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address }))) - } - break - } case SET_DEFAULT_SAFE: { if (action.payload) { saveDefaultSafe(action.payload) diff --git a/src/logic/safe/store/models/owner.ts b/src/logic/safe/store/models/owner.ts deleted file mode 100644 index 835b0df8..00000000 --- a/src/logic/safe/store/models/owner.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Record } from 'immutable' - -export const makeOwner = Record({ - name: 'UNKNOWN', - address: '', -}) - -// Usage const someRecord: Owner = makeOwner({ name: ... }) diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 92617db4..8c72f58b 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -1,11 +1,9 @@ -import { List, Record, RecordOf } from 'immutable' +import { Record, RecordOf } from 'immutable' + import { FEATURES } from 'src/config/networks/network.d' import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' -export type SafeOwner = { - name: string - address: string -} +export type SafeOwner = string export type ModulePair = [ // previous module @@ -25,35 +23,31 @@ export type SpendingLimit = { } export type SafeRecordProps = { - name: string address: string threshold: number ethBalance: string totalFiatBalance: string - owners: List + owners: SafeOwner[] modules?: ModulePair[] | null spendingLimits?: SpendingLimit[] | null balances: BalanceRecord[] nonce: number recurringUser?: boolean - loadedViaUrl?: boolean currentVersion: string needsUpdate: boolean featuresEnabled: Array } const makeSafe = Record({ - name: '', address: '', threshold: 0, ethBalance: '0', totalFiatBalance: '0', - owners: List([]), + owners: [], modules: [], spendingLimits: [], balances: [], nonce: 0, - loadedViaUrl: false, recurringUser: undefined, currentVersion: '', needsUpdate: false, diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index 8602db8f..3175d0e3 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -1,30 +1,22 @@ import { Map, List } from 'immutable' import { Action, handleActions } from 'redux-actions' -import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' -import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' -import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner' -import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner' import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' -import { makeOwner } from 'src/logic/safe/store/models/owner' import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { AppReduxState } from 'src/store' import { checksumAddress } from 'src/utils/checksumAddress' -import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe' +import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated' -import { LOADED_SAFE_KEY } from 'src/utils/constants' export const SAFE_REDUCER_ID = 'safes' export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED' export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { - const names = storedSafe.owners.map((owner) => owner.name) - const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address)) - const owners = buildOwnersFrom(Array.from(names), Array.from(addresses)) + const owners = storedSafe.owners.map(checksumAddress) return { ...storedSafe, @@ -80,17 +72,10 @@ export default handleActions( const safeAddress = safe.address const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress])) - let loadedViaUrl = safe.loadedViaUrl - - if (!state.hasIn(['safes', safeAddress])) { - loadedViaUrl = !safe?.name || safe?.name === LOADED_SAFE_KEY - } return shouldUpdate - ? state.updateIn( - ['safes', safeAddress], - makeSafe({ name: safe?.name || LOADED_SAFE_KEY, address: safeAddress, loadedViaUrl }), - (prevSafe) => updateSafeProps(prevSafe, safe), + ? state.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) => + updateSafeProps(prevSafe, safe), ) : state }, @@ -104,10 +89,8 @@ export default handleActions( const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress])) return shouldUpdate - ? state.updateIn( - ['safes', safeAddress], - makeSafe({ name: safe?.name || LOADED_SAFE_KEY, address: safeAddress, loadedViaUrl: !!safe?.name }), - (prevSafe) => updateSafeProps(prevSafe, safe), + ? state.updateIn(['safes', safeAddress], makeSafe({ address: safeAddress }), (prevSafe) => + updateSafeProps(prevSafe, safe), ) : state }, @@ -123,54 +106,6 @@ export default handleActions( return newState }, - [ADD_SAFE_OWNER]: (state, action: Action) => { - const { ownerAddress, ownerName, safeAddress } = action.payload - - const addressFound = state - .getIn(['safes', safeAddress]) - .owners.find((owner) => sameAddress(owner.address, ownerAddress)) - - if (addressFound) { - return state - } - - return state.updateIn(['safes', safeAddress], (prevSafe) => - prevSafe.merge({ - owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })), - }), - ) - }, - [REMOVE_SAFE_OWNER]: (state, action: Action) => { - const { ownerAddress, safeAddress } = action.payload - - return state.updateIn(['safes', safeAddress], (prevSafe) => - prevSafe.merge({ - owners: prevSafe.owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()), - }), - ) - }, - [REPLACE_SAFE_OWNER]: (state, action: Action) => { - const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload - - return state.updateIn(['safes', safeAddress], (prevSafe) => - prevSafe.merge({ - owners: prevSafe.owners - .filter((o) => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase()) - .push(makeOwner({ address: ownerAddress, name: ownerName })), - }), - ) - }, - [EDIT_SAFE_OWNER]: (state, action: Action) => { - const { ownerAddress, ownerName, safeAddress } = action.payload - - return state.updateIn(['safes', safeAddress], (prevSafe) => { - const ownerToUpdateIndex = prevSafe.owners.findIndex( - (o) => o.address.toLowerCase() === ownerAddress.toLowerCase(), - ) - const updatedOwners = prevSafe.owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName)) - return prevSafe.merge({ owners: updatedOwners }) - }) - }, [SET_DEFAULT_SAFE]: (state, action: Action) => state.set('defaultSafe', action.payload), [SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action) => state.set('latestMasterContractVersion', action.payload), diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 274bdb3a..42d7fa65 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -1,15 +1,20 @@ import { List } from 'immutable' import { matchPath, RouteComponentProps } from 'react-router-dom' import { createSelector } from 'reselect' -import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes' +import { getNetworkId } from 'src/config' import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { AppReduxState } from 'src/store' import { checksumAddress } from 'src/utils/checksumAddress' -import makeSafe, { SafeRecord, SafeRecordProps } from '../models/safe' +import { ETHEREUM_NETWORK } from 'src/config/networks/network' +import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { addressBookMapSelector, addressBookSelector } from 'src/logic/addressBook/store/selectors' +import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes' import { SafesMap } from 'src/routes/safe/store/reducer/types/safe' import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' +import { sameAddress } from 'src/logic/wallets/ethAddresses' const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID] @@ -17,6 +22,32 @@ export const safesMapSelector = (state: AppReduxState): SafesMap => safesStateSe export const safesListSelector = createSelector(safesMapSelector, (safes): List => safes.toList()) +const chainId = getNetworkId() + +type SafeRecordWithName = SafeRecordProps & { name: string } + +export const safesListWithAddressBookNameSelector = createSelector( + [safesListSelector, addressBookMapSelector], + (safesList, addressBookMap): List => { + const addressBook = addressBookMap?.[chainId] + + return safesList + .map((safeRecord) => { + const safe = safeRecord.toObject() + const name = addressBook?.[safe.address]?.name + return { ...safe, name } + }) + .filter((safeRecord: SafeRecordWithName) => safeRecord.name) + }, +) + +export const safeNameSelector = createSelector( + [safesListWithAddressBookNameSelector, (_, safeName: string) => safeName], + (safes, safeAddress): string => { + return safes.find((safe) => sameAddress(safe.address, safeAddress))?.name ?? '' + }, +) + export const safesCountSelector = createSelector(safesMapSelector, (safes) => safes.size) export const defaultSafeSelector = createSelector(safesStateSelector, (safeState) => safeState.get('defaultSafe')) @@ -101,19 +132,24 @@ export const safeFeaturesEnabledSelector = createSelector(safeSelector, safeFiel export const safeSpendingLimitsSelector = createSelector(safeSelector, safeFieldSelector('spendingLimits')) -export const safeLoadedViaUrlSelector = createSelector(safeSelector, safeFieldSelector('loadedViaUrl')) - -export const safeOwnersAddressesListSelector = createSelector( - safeOwnersSelector, - (owners): List => { - if (!owners) { - return List([]) - } - - return owners?.map(({ address }) => address) - }, -) - export const safeTotalFiatBalanceSelector = createSelector(safeSelector, (currentSafe) => { return currentSafe?.totalFiatBalance }) + +export const safeOwnersWithAddressBookDataSelector = createSelector( + [safeOwnersSelector, addressBookSelector, (_, chainId: ETHEREUM_NETWORK) => chainId], + (owners, addressBook, chainId): AppReduxState['addressBook'] | undefined => + owners?.map((ownerAddress) => { + const ownerInAddressBook = addressBook.find( + (addressBookEntry) => + sameAddress(ownerAddress, addressBookEntry.address) && chainId === addressBookEntry.chainId, + ) + + if (ownerInAddressBook) { + return ownerInAddressBook + } + + // if there's no owner's data in the AB, we create an in-memory AB-like structure + return makeAddressBookEntry({ address: ownerAddress, name: '' }) + }), +) diff --git a/src/logic/safe/transactions/notifiedTransactions.ts b/src/logic/safe/transactions/notifiedTransactions.ts index 9f18650e..a71d9d14 100644 --- a/src/logic/safe/transactions/notifiedTransactions.ts +++ b/src/logic/safe/transactions/notifiedTransactions.ts @@ -8,8 +8,8 @@ export const TX_NOTIFICATION_TYPES = { REMOVE_SPENDING_LIMIT_TX: 'REMOVE_SPENDING_LIMIT_TX', SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX', OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX', - ADDRESSBOOK_NEW_ENTRY: 'ADDRESSBOOK_NEW_ENTRY', - ADDRESSBOOK_EDIT_ENTRY: 'ADDRESSBOOK_EDIT_ENTRY', - ADDRESSBOOK_DELETE_ENTRY: 'ADDRESSBOOK_DELETE_ENTRY', - ADDRESSBOOK_EXPORT_ENTRIES: 'ADDRESSBOOK_EXPORT_ENTRIES', + ADDRESS_BOOK_NEW_ENTRY: 'ADDRESS_BOOK_NEW_ENTRY', + ADDRESS_BOOK_EDIT_ENTRY: 'ADDRESS_BOOK_EDIT_ENTRY', + ADDRESS_BOOK_DELETE_ENTRY: 'ADDRESS_BOOK_DELETE_ENTRY', + ADDRESS_BOOK_EXPORT_ENTRIES: 'ADDRESS_BOOK_EXPORT_ENTRIES', } diff --git a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts index 5ee2dd4e..50ccee75 100644 --- a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts +++ b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts @@ -12,28 +12,20 @@ const getMockedOldSafe = ({ currentVersion, ethBalance, threshold, - name, nonce, modules, spendingLimits, }: Partial): SafeRecordProps => { - const owner1 = { - name: 'MockedOwner1', - address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d', - } - const owner2 = { - name: 'MockedOwner2', - address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3', - } + const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d' + const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3' const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' return { - name: name || 'MockedSafe', address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F', threshold: threshold || 2, ethBalance: ethBalance || '10', - owners: owners || List([owner1, owner2]), + owners: owners || [owner1, owner2], modules: modules || [], spendingLimits: spendingLimits || [], balances: balances || [ @@ -75,21 +67,6 @@ describe('shouldSafeStoreBeUpdated', () => { // Then expect(expectedResult).toEqual(true) }) - it(`Given an old safe and a new name for the safe, should return true`, () => { - // given - const oldName = 'oldName' - const newName = 'newName' - const oldSafe = getMockedOldSafe({ name: oldName }) - const newSafeProps: Partial = { - name: newName, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) it(`Given an old safe and a new threshold for the safe, should return true`, () => { // given const oldThreshold = 1 @@ -122,18 +99,10 @@ describe('shouldSafeStoreBeUpdated', () => { }) it(`Given an old owners list and a new owners list for the safe, should return true`, () => { // given - const owner1 = { - name: 'MockedOwner1', - address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d', - } - const owner2 = { - name: 'MockedOwner2', - address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3', - } - const oldSafe = getMockedOldSafe({ owners: List([owner1, owner2]) }) - const newSafeProps: Partial = { - owners: List([owner1]), - } + const owner1 = '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d' + const owner2 = '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3' + const oldSafe = getMockedOldSafe({ owners: [owner1, owner2] }) + const newSafeProps: Partial = { owners: [owner1] } // When const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) @@ -146,9 +115,7 @@ describe('shouldSafeStoreBeUpdated', () => { const oldModulesList = [] const newModulesList = null const oldSafe = getMockedOldSafe({ modules: oldModulesList }) - const newSafeProps: Partial = { - modules: newModulesList, - } + const newSafeProps: Partial = { modules: newModulesList } // When const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) @@ -161,9 +128,7 @@ describe('shouldSafeStoreBeUpdated', () => { const oldSpendingLimitsList = [] const newSpendingLimitsList = null const oldSafe = getMockedOldSafe({ spendingLimits: oldSpendingLimitsList }) - const newSafeProps: Partial = { - modules: newSpendingLimitsList, - } + const newSafeProps: Partial = { modules: newSpendingLimitsList } // When const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) diff --git a/src/logic/safe/utils/safeStorage.ts b/src/logic/safe/utils/safeStorage.ts index 73d15185..725cb818 100644 --- a/src/logic/safe/utils/safeStorage.ts +++ b/src/logic/safe/utils/safeStorage.ts @@ -4,7 +4,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe' export const SAFES_KEY = 'SAFES' export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE' -type StoredSafes = Record +export type StoredSafes = Record export const loadStoredSafes = (): Promise => { return loadFromStorage(SAFES_KEY) diff --git a/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts b/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts index d8adaa4c..caf2295a 100644 --- a/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts +++ b/src/logic/safe/utils/shouldSafeStoreBeUpdated.ts @@ -6,7 +6,7 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe' const isStateSubset = (superObj, subObj) => { return Object.keys(subObj).every((key) => { if (subObj[key] && typeof subObj[key] == 'object') { - if (typeof subObj[key] === 'object' || subObj[key].length >= 0) { + if (typeof subObj[key] === 'object' || subObj[key].size >= 0) { // If type is Immutable Map, List or Object we use Immutable equals return isEqual(superObj[key], subObj[key]) } diff --git a/src/logic/wallets/__tests__/ethAddresses.test.ts b/src/logic/wallets/__tests__/ethAddresses.test.ts index 28bb962b..84c728b4 100644 --- a/src/logic/wallets/__tests__/ethAddresses.test.ts +++ b/src/logic/wallets/__tests__/ethAddresses.test.ts @@ -9,7 +9,6 @@ import { shortVersionOf, } from 'src/logic/wallets/ethAddresses' import makeSafe from 'src/logic/safe/store/models/safe' -import { makeOwner } from 'src/logic/safe/store/models/owner' describe('src/logic/wallets/ethAddresses', () => { describe('Utility function: sameAddress', () => { @@ -113,7 +112,7 @@ describe('src/logic/wallets/ethAddresses', () => { it("Should return false if there's no `userAccount`", () => { // given const userAddress = null - const owners = List([makeOwner({ address: userAddress })]) + const owners = [userAddress] const safeInstance = makeSafe({ owners }) const expectedResult = false @@ -139,7 +138,7 @@ describe('src/logic/wallets/ethAddresses', () => { it("Should return true if `userAccount` is not in the list of Safe's owners", () => { // given const userAddress = 'address1' - const owners = List([makeOwner({ address: userAddress })]) + const owners = [userAddress] const safeInstance = makeSafe({ owners }) const expectedResult = true @@ -153,7 +152,7 @@ describe('src/logic/wallets/ethAddresses', () => { // given const userAddress = 'address1' const userAddress2 = 'address2' - const owners = List([makeOwner({ address: userAddress })]) + const owners = [userAddress] const safeInstance = makeSafe({ owners }) const expectedResult = false @@ -170,8 +169,8 @@ describe('src/logic/wallets/ethAddresses', () => { // given const userAddress = 'address1' const userAddress2 = 'address2' - const owners1 = List([makeOwner({ address: userAddress })]) - const owners2 = List([makeOwner({ address: userAddress2 })]) + const owners1 = [userAddress] + const owners2 = [userAddress2] const safeInstance = makeSafe({ owners: owners1 }) const safeInstance2 = makeSafe({ owners: owners2 }) const safesList = List([safeInstance, safeInstance2]) @@ -188,8 +187,8 @@ describe('src/logic/wallets/ethAddresses', () => { const userAddress = 'address1' const userAddress2 = 'address2' const userAddress3 = 'address3' - const owners1 = List([makeOwner({ address: userAddress3 })]) - const owners2 = List([makeOwner({ address: userAddress2 })]) + const owners1 = [userAddress3] + const owners2 = [userAddress2] const safeInstance = makeSafe({ owners: owners1 }) const safeInstance2 = makeSafe({ owners: owners2 }) const safesList = List([safeInstance, safeInstance2]) diff --git a/src/logic/wallets/ethAddresses.ts b/src/logic/wallets/ethAddresses.ts index 79ecfd43..d88f4e31 100644 --- a/src/logic/wallets/ethAddresses.ts +++ b/src/logic/wallets/ethAddresses.ts @@ -40,7 +40,7 @@ export const isUserAnOwner = (safe: SafeRecord, userAccount: string): boolean => return false } - return owners.find((owner) => sameAddress(owner.address, userAccount)) !== undefined + return owners.find((address) => sameAddress(address, userAccount)) !== undefined } export const isUserAnOwnerOfAnySafe = (safes: List | SafeRecord[], userAccount: string): boolean => diff --git a/src/routes/load/container/Load.tsx b/src/routes/load/container/Load.tsx index 10c6d9de..96044d5e 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -1,31 +1,25 @@ -import * as React from 'react' +import React, { ReactElement } from 'react' import { useDispatch, useSelector } from 'react-redux' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import Layout from 'src/routes/load/components/Layout' -import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME } from '../components/fields' +import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions' +import { FIELD_LOAD_ADDRESS } from 'src/routes/load/components/fields' import Page from 'src/components/layout/Page' -import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { saveSafes, loadStoredSafes } from 'src/logic/safe/utils' -import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor' +import { getAccountsFrom, getNamesFrom } from 'src/routes/open/utils/safeDataExtractor' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' import { history } from 'src/store' -import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe' -import { List } from 'immutable' +import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { checksumAddress } from 'src/utils/checksumAddress' import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' -export const loadSafe = async ( - safeName: string, - safeAddress: string, - owners: List, - addSafe: (safe: SafeRecordProps) => void, -): Promise => { - const safeProps = await buildSafe(safeAddress, safeName) - safeProps.owners = owners +export const loadSafe = async (safeAddress: string, addSafe: (safe: SafeRecordProps) => void): Promise => { + const safeProps = await buildSafe(safeAddress) const storedSafes = (await loadStoredSafes()) || {} @@ -53,7 +47,7 @@ interface LoadForm { export type LoadFormValues = ReviewSafeCreationValues | LoadForm -const Load = (): React.ReactElement => { +const Load = (): ReactElement => { const dispatch = useDispatch() const provider = useSelector(providerNameSelector) const network = useSelector(networkSelector) @@ -64,22 +58,27 @@ const Load = (): React.ReactElement => { } const onLoadSafeSubmit = async (values: LoadFormValues) => { let safeAddress = values[FIELD_LOAD_ADDRESS] - // TODO: review this check. It doesn't seems to be necessary at this point + if (!safeAddress) { console.error('failed to add Safe address', JSON.stringify(values)) return } + const ownersNames = getNamesFrom(values) + const ownersAddresses = getAccountsFrom(values) + + const owners = ownersAddresses.map((address, index) => + makeAddressBookEntry({ + address, + name: ownersNames[index], + }), + ) + const safe = makeAddressBookEntry({ address: safeAddress, name: values.name }) + await dispatch(addressBookSafeLoad([...owners, safe])) + try { - const safeName = values[FIELD_LOAD_NAME] safeAddress = checksumAddress(safeAddress) - const ownerNames = getNamesFrom(values) - - const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) - const ownerAddresses = await gnosisSafe.methods.getOwners().call() - const owners = getOwnersFrom(ownerNames, ownerAddresses.slice().sort()) - - await loadSafe(safeName, safeAddress, owners, addSafeHandler) + await loadSafe(safeAddress, addSafeHandler) const url = `${SAFELIST_ADDRESS}/${safeAddress}/balances` history.push(url) diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index cc6b2e53..eac4a9be 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -1,7 +1,7 @@ import { Loader } from '@gnosis.pm/safe-react-components' import { backOff } from 'exponential-backoff' import queryString from 'query-string' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, ReactElement } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useLocation } from 'react-router-dom' import { PromiEvent, TransactionReceipt } from 'web3-core' @@ -16,7 +16,6 @@ import { CreateSafeValues, getAccountsFrom, getNamesFrom, - getOwnersFrom, getSafeCreationSaltFrom, getSafeNameFrom, getThresholdFrom, @@ -25,8 +24,9 @@ import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' import { buildSafe } from 'src/logic/safe/store/actions/fetchSafe' import { history } from 'src/store' import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/storage' +import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions' import { userAccountSelector } from 'src/logic/wallets/store/selectors' -import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { useAnalytics } from 'src/utils/googleAnalytics' import { sleep } from 'src/utils/timer' @@ -78,18 +78,6 @@ const getSafePropsValuesFromQueryParams = (queryParams: SafeCreationQueryParams) } } -export const getSafeProps = async ( - safeAddress: string, - safeName: string, - ownersNames: string[], - ownerAddresses: string[], -): Promise => { - const safeProps = await buildSafe(safeAddress, safeName) - safeProps.owners = getOwnersFrom(ownersNames, ownerAddresses) - - return safeProps -} - export const createSafe = (values: CreateSafeValues, userAccount: string): PromiEvent => { const confirmations = getThresholdFrom(values) const ownerAddresses = getAccountsFrom(values) @@ -118,7 +106,7 @@ export const createSafe = (values: CreateSafeValues, userAccount: string): Promi return promiEvent } -const Open = (): React.ReactElement => { +const Open = (): ReactElement => { const [loading, setLoading] = useState(false) const [showProgress, setShowProgress] = useState(false) const [creationTxPromise, setCreationTxPromise] = useState>() @@ -179,13 +167,23 @@ const Open = (): React.ReactElement => { const onSafeCreated = async (safeAddress): Promise => { const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - const name = pendingCreation ? getSafeNameFrom(pendingCreation) : '' - const ownersNames = getNamesFrom(pendingCreation as CreateSafeValues) - const ownerAddresses = pendingCreation ? getAccountsFrom(pendingCreation) : [] - const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses) + let name = '' + let ownersNames: string[] = [] + let ownersAddresses: string[] = [] + if (pendingCreation) { + name = getSafeNameFrom(pendingCreation) + ownersNames = getNamesFrom(pendingCreation as CreateSafeValues) + ownersAddresses = getAccountsFrom(pendingCreation) + } + + const safeProps = await buildSafe(safeAddress) await dispatch(addOrUpdateSafe(safeProps)) + const owners = ownersAddresses.map((address, index) => makeAddressBookEntry({ address, name: ownersNames[index] })) + const safe = makeAddressBookEntry({ address: safeAddress, name }) + await dispatch(addressBookSafeLoad([...owners, safe])) + trackEvent({ category: 'User', action: 'Created a safe', diff --git a/src/routes/open/utils/safeDataExtractor.ts b/src/routes/open/utils/safeDataExtractor.ts index e44452c8..26044ee1 100644 --- a/src/routes/open/utils/safeDataExtractor.ts +++ b/src/routes/open/utils/safeDataExtractor.ts @@ -1,7 +1,3 @@ -import { List } from 'immutable' - -import { makeOwner } from 'src/logic/safe/store/models/owner' -import { SafeOwner } from 'src/logic/safe/store/models/safe' import { LoadFormValues } from 'src/routes/load/container/Load' import { getNumOwnersFrom } from 'src/routes/open/components/fields' @@ -15,29 +11,18 @@ export type CreateSafeValues = { owners?: number | string } -export const getAccountsFrom = (values: CreateSafeValues | LoadFormValues): string[] => { +const getByRegexFrom = (regex: RegExp) => (values: CreateSafeValues | LoadFormValues): string[] => { const accounts = Object.keys(values) .sort() - .filter((key) => /^owner\d+Address$/.test(key)) + .filter((key) => regex.test(key)) const numOwners = getNumOwnersFrom(values) return accounts.map((account) => values[account]).slice(0, numOwners) } -export const getNamesFrom = (values: CreateSafeValues | LoadFormValues): string[] => { - const accounts = Object.keys(values) - .sort() - .filter((key) => /^owner\d+Name$/.test(key)) +export const getAccountsFrom = getByRegexFrom(/^owner\d+Address$/) - const numOwners = getNumOwnersFrom(values) - return accounts.map((account) => values[account]).slice(0, numOwners) -} - -export const getOwnersFrom = (names: string[], addresses: string[]): List => { - const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] })) - - return List(owners) -} +export const getNamesFrom = getByRegexFrom(/^owner\d+Name$/) export const getThresholdFrom = (values: CreateSafeValues): number => Number(values.confirmations) diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx index 660bf3f0..143d107f 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx @@ -74,7 +74,7 @@ export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps) //This timeout prevents modal to be closed abruptly setTimeout(() => { if (!loading) { - const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_EXPORT_ENTRIES) + const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EXPORT_ENTRIES) const action = error ? notification.afterExecution.afterExecutionError : notification.afterExecution.noMoreConfirmationsNeeded diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 99ee50b2..d36929f5 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -10,16 +10,14 @@ import { useDispatch, useSelector } from 'react-redux' import { styles } from './style' -import { getExplorerInfo } from 'src/config' +import { getExplorerInfo, getNetworkId } from 'src/config' import Table from 'src/components/Table' import { cellWidth } from 'src/components/Table/TableHead' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry' -import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry' -import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' +import { addressBookAddOrUpdate, addressBookRemove } from 'src/logic/addressBook/store/actions' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses' import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal' @@ -44,6 +42,7 @@ const StyledButton = styled(Button)` padding: 0 12px; min-width: auto; } + svg { margin: 0 6px 0 0; } @@ -71,7 +70,8 @@ export type Entry = { isOwnerAddress?: boolean } -const initialEntryState: Entry = { entry: { address: '', name: '', isNew: true } } +const chainId = getNetworkId() +const initialEntryState: Entry = { entry: { address: '', name: '', chainId, isNew: true } } const AddressBookTable = (): ReactElement => { const classes = useStyles() @@ -114,6 +114,7 @@ const AddressBookTable = (): ReactElement => { entry: { name: '', address, + chainId, isNew: true, }, }) @@ -122,29 +123,28 @@ const AddressBookTable = (): ReactElement => { }, [addressBook, entryAddressToEditOrCreateNew]) const newEntryModalHandler = (entry: AddressBookEntry) => { + // close the modal setEditCreateEntryModalOpen(false) - const checksumEntries = { - ...entry, - address: checksumAddress(entry.address), - } - dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries))) + // update the store + dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry))) } const editEntryModalHandler = (entry: AddressBookEntry) => { + // reset the form setSelectedEntry(initialEntryState) + // close the modal setEditCreateEntryModalOpen(false) - const checksumEntries = { - ...entry, - address: checksumAddress(entry.address), - } - dispatch(updateAddressBookEntry(makeAddressBookEntry(checksumEntries))) + // update the store + dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry))) } const deleteEntryModalHandler = () => { - const entryAddress = selectedEntry?.entry ? checksumAddress(selectedEntry.entry.address) : '' + // reset the form setSelectedEntry(initialEntryState) + // close the modal setDeleteEntryModalOpen(false) - dispatch(removeAddressBookEntry(entryAddress)) + // update the store + selectedEntry?.entry && dispatch(addressBookRemove(selectedEntry.entry)) } const importEntryModalHandler = (addressList: AddressBookEntry[]) => { @@ -153,7 +153,7 @@ const AddressBookTable = (): ReactElement => { ...entry, address: checksumAddress(entry.address), } - dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries))) + dispatch(addressBookAddOrUpdate(makeAddressBookEntry(checksumEntries))) }) setImportEntryModalOpen(false) } diff --git a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx index 27f5ad53..947af7dd 100644 --- a/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/SafeInfo/index.tsx @@ -4,11 +4,12 @@ import { EthHashInfo } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' import { getExplorerInfo, getNetworkInfo } from 'src/config' -import { safeSelector } from 'src/logic/safe/store/selectors' +import { safeNameSelector, safeSelector } from 'src/logic/safe/store/selectors' import Paragraph from 'src/components/layout/Paragraph' import Bold from 'src/components/layout/Bold' import { border, xs } from 'src/theme/variables' import Block from 'src/components/layout/Block' + const { nativeCoin } = getNetworkInfo() const StyledBlock = styled(Block)` @@ -24,7 +25,8 @@ const StyledBlock = styled(Block)` ` const SafeInfo = (): React.ReactElement => { - const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {} + const { address: safeAddress = '', ethBalance } = useSelector(safeSelector) || {} + const safeName = useSelector((state) => safeNameSelector(state, safeAddress)) return ( <> diff --git a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx index a9280813..cbd1cd6a 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -5,7 +5,7 @@ import React, { Dispatch, ReactElement, SetStateAction, useEffect, useState } fr import { useSelector } from 'react-redux' import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator' -import { isFeatureEnabled } from 'src/config' +import { getNetworkId, isFeatureEnabled } from 'src/config' import { FEATURES } from 'src/config/networks/network.d' import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' @@ -18,6 +18,8 @@ import { } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/style' import { trimSpaces } from 'src/utils/strings' +const chainId = getNetworkId() + export interface AddressBookProps { fieldMutator: (address: string) => void label?: string @@ -65,8 +67,8 @@ const BaseAddressBookInput = ({ const onChange: AutocompleteProps['onChange'] = (_, value, reason) => { switch (reason) { case 'select-option': { - const { address, name } = value as AddressBookEntry - updateAddressInfo({ address, name }) + const { address, name, chainId } = value as AddressBookEntry + updateAddressInfo({ address, name, chainId }) break } } @@ -99,7 +101,14 @@ const BaseAddressBookInput = ({ break } - const newEntry = typeof validatedAddress === 'string' ? { address, name: normalizedValue } : validatedAddress + const newEntry = + typeof validatedAddress === 'string' + ? { + address, + name: normalizedValue, + chainId, + } + : validatedAddress updateAddressInfo(newEntry) break @@ -114,7 +123,13 @@ const BaseAddressBookInput = ({ } const newEntry = - typeof validatedAddress === 'string' ? { address: validatedAddress, name: '' } : validatedAddress + typeof validatedAddress === 'string' + ? { + address: validatedAddress, + name: '', + chainId, + } + : validatedAddress updateAddressInfo(newEntry) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 67035766..a06dae5b 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -2,10 +2,9 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import Modal from 'src/components/Modal' -import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' @@ -46,7 +45,7 @@ export const sendAddOwner = async ( ) if (txHash) { - dispatch(addSafeOwner({ safeAddress, ownerName: values.ownerName, ownerAddress: values.ownerAddress })) + dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName }))) } } @@ -99,9 +98,7 @@ export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement => try { await sendAddOwner(values, safeAddress, txParameters, dispatch) - dispatch( - addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })), - ) + dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress }))) } catch (error) { console.error('Error while removing an owner', error) } diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx index 8e211801..f04f5207 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx @@ -1,11 +1,14 @@ import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' +import { Mutator } from 'final-form' import React from 'react' import { useSelector } from 'react-redux' +import { OnChange } from 'react-final-form-listeners' import { styles } from './style' +import { getNetworkId } from 'src/config' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' @@ -24,7 +27,9 @@ 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 { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors' +import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { OwnerValues } from '../..' @@ -32,14 +37,22 @@ export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input' export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' export const ADD_OWNER_NEXT_BTN_TEST_ID = 'add-owner-next-btn' -const formMutators = { +const formMutators: Record< + string, + Mutator<{ setOwnerAddress: { address: string }; setOwnerName: { name: string } }> +> = { setOwnerAddress: (args, state, utils) => { utils.changeValue(state, 'ownerAddress', () => args[0]) }, + setOwnerName: (args, state, utils) => { + utils.changeValue(state, 'ownerName', () => args[0]) + }, } const useStyles = makeStyles(styles) +const chainId = getNetworkId() + type OwnerFormProps = { onClose: () => void onSubmit: (values) => void @@ -51,7 +64,8 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps): const handleSubmit = (values) => { onSubmit(values) } - const owners = useSelector(safeOwnersAddressesListSelector) + const addressBookMap = useSelector(addressBookMapSelector) + const owners = useSelector(safeOwnersSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) const ownerDoesntExist = uniqueAddress(owners) const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress) @@ -106,6 +120,16 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps): type="text" validate={composeValidators(required, minMaxLength(1, 50))} /> + + {async (address: string) => { + if (web3ReadOnly.utils.isAddress(address)) { + const { name: ownerName } = addressBookMap[chainId][address] + if (ownerName) { + mutators.setOwnerName(ownerName) + } + } + }} + diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index e630d723..22a21b48 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -5,7 +5,7 @@ import React, { ReactElement, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { EthHashInfo } from '@gnosis.pm/safe-react-components' -import { getExplorerInfo } from 'src/config' +import { getExplorerInfo, getNetworkId } from 'src/config' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' @@ -14,7 +14,10 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' -import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { + safeOwnersWithAddressBookDataSelector, + safeParamAddressFromStateSelector, +} from 'src/logic/safe/store/selectors' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' @@ -28,6 +31,8 @@ export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' const useStyles = makeStyles(styles) +const chainId = getNetworkId() + type ReviewAddOwnerProps = { onClickBack: () => void onClose: () => void @@ -40,7 +45,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie const [data, setData] = useState('') const safeAddress = useSelector(safeParamAddressFromStateSelector) as string const safeName = useSafeName(safeAddress) - const owners = useSelector(safeOwnersSelector) + const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId)) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() const [manualGasLimit, setManualGasLimit] = useState() @@ -146,7 +151,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie Any transaction requires the confirmation of: - {`${values.threshold} out of ${(owners?.size || 0) + 1} owner(s)`} + {`${values.threshold} out of ${(owners?.length || 0) + 1} owner(s)`} @@ -154,7 +159,7 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie - {`${(owners?.size || 0) + 1} Safe owner(s)`} + {`${(owners?.length || 0) + 1} Safe owner(s)`} diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx index 5851ed0e..79db30a0 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx @@ -42,6 +42,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: const classes = useStyles() const threshold = useSelector(safeThresholdSelector) as number const owners = useSelector(safeOwnersSelector) + const numOptions = owners ? owners.length + 1 : 0 const handleSubmit = (values: SubmitProps) => { onSubmit(values) @@ -79,7 +80,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: render={(props) => ( <> - {[...Array(Number(owners ? owners.size + 1 : 0))].map((x, index) => ( + {[...Array(Number(numOptions))].map((x, index) => ( {index + 1} @@ -92,17 +93,12 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: )} )} - validate={composeValidators( - required, - mustBeInteger, - minValue(1), - maxValue(owners ? owners.size + 1 : 0), - )} + validate={composeValidators(required, mustBeInteger, minValue(1), maxValue(numOptions))} /> - out of {owners ? owners.size + 1 : 0} owner(s) + out of {numOptions} owner(s) diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index 9fc05b99..5d23d768 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -2,7 +2,7 @@ import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' @@ -15,11 +15,9 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import Modal from 'src/components/Modal' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions' import { NOTIFICATIONS } from 'src/logic/notifications' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' -import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { styles } from './style' import { getExplorerInfo } from 'src/config' @@ -40,13 +38,11 @@ type OwnProps = { export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => { const classes = useStyles() const dispatch = useDispatch() - const safeAddress = useSelector(safeParamAddressFromStateSelector) const handleSubmit = ({ ownerName }: { ownerName: string }): void => { // Update the value only if the ownerName really changed if (ownerName !== selectedOwnerName) { - dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName })) - dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName }))) + dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: ownerAddress, name: ownerName }))) dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG)) } onClose() diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index ebdc8db9..0e39ed50 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -9,8 +9,7 @@ import Modal from 'src/components/Modal' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' -import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner' -import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { Dispatch } from 'src/logic/safe/store/actions/types.d' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' @@ -27,7 +26,6 @@ export const sendRemoveOwner = async ( ownerNameToRemove: string, dispatch: Dispatch, txParameters: TxParameters, - threshold?: number, ): Promise => { const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.methods.getOwners().call() @@ -37,7 +35,7 @@ export const sendRemoveOwner = async ( const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddressToRemove, values.threshold).encodeABI() - const txHash = await dispatch( + dispatch( createTransaction({ safeAddress, to: safeAddress, @@ -49,10 +47,6 @@ export const sendRemoveOwner = async ( notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, }), ) - - if (txHash && threshold === 1) { - dispatch(removeSafeOwner({ safeAddress, ownerAddress: ownerAddressToRemove })) - } } type RemoveOwnerProps = { @@ -72,7 +66,6 @@ export const RemoveOwnerModal = ({ const [values, setValues] = useState({ ownerAddress, ownerName, threshold: '' }) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const threshold = useSelector(safeThresholdSelector) || 1 useEffect( () => () => { @@ -101,7 +94,7 @@ export const RemoveOwnerModal = ({ const onRemoveOwner = (txParameters: TxParameters) => { onClose() - sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters, threshold) + sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters) } return ( 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 13db0932..a9fcb219 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 @@ -4,9 +4,8 @@ import Close from '@material-ui/icons/Close' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { EthHashInfo } from '@gnosis.pm/safe-react-components' -import { List } from 'immutable' -import { getExplorerInfo } from 'src/config' +import { getExplorerInfo, getNetworkId } from 'src/config' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' @@ -14,10 +13,11 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' -import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { + safeOwnersWithAddressBookDataSelector, + safeParamAddressFromStateSelector, +} from 'src/logic/safe/store/selectors' import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' -import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' -import { addressBookSelector } from 'src/logic/addressBook/store/selectors' 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' @@ -31,6 +31,8 @@ export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' const useStyles = makeStyles(styles) +const chainId = getNetworkId() + type ReviewRemoveOwnerProps = { onClickBack: () => void onClose: () => void @@ -52,9 +54,8 @@ export const ReviewRemoveOwnerModal = ({ const [data, setData] = useState('') const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSafeName(safeAddress) - const owners = useSelector(safeOwnersSelector) - const addressBook = useSelector(addressBookSelector) - const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) + const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId)) + const numOptions = owners ? owners.length - 1 : 0 const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() const [manualGasLimit, setManualGasLimit] = useState() @@ -86,6 +87,8 @@ export const ReviewRemoveOwnerModal = ({ const calculateRemoveOwnerData = async () => { try { + // FixMe: if the order returned by the service is the same as in the contracts + // 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)) @@ -169,7 +172,7 @@ export const ReviewRemoveOwnerModal = ({ Any transaction requires the confirmation of: - {`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`} + {`${threshold} out of ${numOptions} owner(s)`} @@ -178,11 +181,11 @@ export const ReviewRemoveOwnerModal = ({ - {`${owners ? owners.size - 1 : 0} Safe owner(s)`} + {`${numOptions} Safe owner(s)`} - {ownersWithAddressBookName?.map( + {owners?.map( (owner) => owner.address !== ownerAddress && ( diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx index 3fc4c914..58068c69 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx @@ -38,6 +38,7 @@ type Props = { export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: Props): ReactElement => { const classes = useStyles() const owners = useSelector(safeOwnersSelector) + const ownersCount = owners?.length ?? 0 const threshold = useSelector(safeThresholdSelector) as number const handleSubmit = (values) => { onSubmit(values) @@ -61,7 +62,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: onSubmit={handleSubmit} > {() => { - const numOptions = owners && owners.size > 1 ? owners.size - 1 : 1 + const numOptions = ownersCount > 1 ? ownersCount - 1 : 1 return ( <> @@ -100,7 +101,7 @@ export const ThresholdForm = ({ onClickBack, onClose, onSubmit, initialValues }: - out of {owners ? owners.size - 1 : 0} owner(s) + out of {ownersCount ? ownersCount - 1 : 0} owner(s) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 2cb1b18d..b43ea806 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -2,12 +2,11 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import Modal from 'src/components/Modal' -import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' -import { replaceSafeOwner } from 'src/logic/safe/store/actions/replaceSafeOwner' -import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { sameAddress } from 'src/logic/wallets/ethAddresses' @@ -28,7 +27,6 @@ export const sendReplaceOwner = async ( ownerAddressToRemove: string, dispatch: Dispatch, txParameters: TxParameters, - threshold?: number, ): Promise => { const gnosisSafe = getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.methods.getOwners().call() @@ -49,14 +47,15 @@ export const sendReplaceOwner = async ( }), ) - if (txHash && threshold === 1) { + if (txHash) { + // update the AB dispatch( - replaceSafeOwner({ - safeAddress, - oldOwnerAddress: ownerAddressToRemove, - ownerAddress: values.newOwnerAddress, - ownerName: values.newOwnerName, - }), + addressBookAddOrUpdate( + makeAddressBookEntry({ + address: values.newOwnerAddress, + name: values.newOwnerName, + }), + ), ) } } @@ -81,7 +80,6 @@ export const ReplaceOwnerModal = ({ }) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const threshold = useSelector(safeThresholdSelector) || 1 useEffect( () => () => { @@ -109,12 +107,10 @@ export const ReplaceOwnerModal = ({ const onReplaceOwner = async (txParameters: TxParameters) => { onClose() try { - await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters, threshold) + await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters) dispatch( - addOrUpdateAddressBookEntry( - makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName }), - ), + addressBookAddOrUpdate(makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName })), ) } catch (error) { console.error('Error while removing an owner', error) 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 2661b60f..18ca0335 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 @@ -1,7 +1,9 @@ import IconButton from '@material-ui/core/IconButton' import Close from '@material-ui/icons/Close' +import { Mutator } from 'final-form' import React, { ReactElement } from 'react' import { useSelector } from 'react-redux' +import { OnChange } from 'react-final-form-listeners' import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' @@ -21,10 +23,12 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' -import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors' +import { web3ReadOnly } from 'src/logic/wallets/getWeb3' import { styles } from './style' -import { getExplorerInfo } from 'src/config' +import { getExplorerInfo, getNetworkId } from 'src/config' import { EthHashInfo } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core' @@ -34,14 +38,22 @@ export const REPLACE_OWNER_NEXT_BTN_TEST_ID = 'replace-owner-next-btn' import { OwnerValues } from '../..' -const formMutators = { +const formMutators: Record< + string, + Mutator<{ setOwnerAddress: { address: string }; setOwnerName: { name: string } }> +> = { setOwnerAddress: (args, state, utils) => { utils.changeValue(state, 'ownerAddress', () => args[0]) }, + setOwnerName: (args, state, utils) => { + utils.changeValue(state, 'ownerName', () => args[0]) + }, } const useStyles = makeStyles(styles) +const chainId = getNetworkId() + type NewOwnerProps = { ownerAddress: string ownerName: string @@ -67,9 +79,11 @@ export const OwnerForm = ({ const handleSubmit = (values: NewOwnerProps) => { onSubmit(values) } - const owners = useSelector(safeOwnersAddressesListSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) + const addressBookMap = useSelector(addressBookMapSelector) + const owners = useSelector(safeOwnersSelector) const ownerDoesntExist = uniqueAddress(owners) + + const safeAddress = useSelector(safeParamAddressFromStateSelector) const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress) return ( @@ -143,6 +157,16 @@ export const OwnerForm = ({ type="text" validate={composeValidators(required, minMaxLength(1, 50))} /> + + {async (address: string) => { + if (web3ReadOnly.utils.isAddress(address)) { + const ownerName = addressBookMap?.[chainId]?.[address]?.name + if (ownerName) { + mutators.setOwnerName(ownerName) + } + } + }} + 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 e1a1f83c..f07e01fc 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 @@ -3,10 +3,9 @@ import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { List } from 'immutable' import { EthHashInfo } from '@gnosis.pm/safe-react-components' -import { getExplorerInfo } from 'src/config' +import { getExplorerInfo, getNetworkId } from 'src/config' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' @@ -15,13 +14,11 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { - safeOwnersSelector, + safeOwnersWithAddressBookDataSelector, safeParamAddressFromStateSelector, safeThresholdSelector, } from 'src/logic/safe/store/selectors' import { useSafeName } from 'src/logic/addressBook/hooks/useSafeName' -import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' -import { addressBookSelector } from 'src/logic/addressBook/store/selectors' 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' @@ -34,6 +31,8 @@ export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn' const useStyles = makeStyles(styles) +const chainId = getNetworkId() + type ReplaceOwnerProps = { onClose: () => void onClickBack: () => void @@ -58,10 +57,8 @@ export const ReviewReplaceOwnerModal = ({ const [data, setData] = useState('') const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSafeName(safeAddress) - const owners = useSelector(safeOwnersSelector) + const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId)) const threshold = useSelector(safeThresholdSelector) || 1 - const addressBook = useSelector(addressBookSelector) - const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() const [manualGasLimit, setManualGasLimit] = useState() @@ -164,7 +161,7 @@ export const ReviewReplaceOwnerModal = ({ Any transaction requires the confirmation of: - {`${threshold} out of ${owners?.size || 0} owner(s)`} + {`${threshold} out of ${owners?.length || 0} owner(s)`} @@ -172,11 +169,11 @@ export const ReviewReplaceOwnerModal = ({ - {`${owners?.size || 0} Safe owner(s)`} + {`${owners?.length || 0} Safe owner(s)`} - {ownersWithAddressBookName?.map( + {owners?.map( (owner) => owner.address !== ownerAddress && ( diff --git a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts index bf1e396f..171c7499 100644 --- a/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts +++ b/src/routes/safe/components/Settings/ManageOwners/dataFetcher.ts @@ -1,12 +1,13 @@ import { List } from 'immutable' + import { TableColumn } from 'src/components/Table/types.d' -import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' 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: List): List<{ address: string; name: string }> => { +export const getOwnerData = (owners: AddressBookState): { address: string; name: string }[] => { 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 229e9d9a..555dd661 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -5,7 +5,6 @@ import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' -import { List } from 'immutable' import RemoveOwnerIcon from '../assets/icons/bin.svg' @@ -29,10 +28,8 @@ import Heading from 'src/components/layout/Heading' import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph/index' import Row from 'src/components/layout/Row' -import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' -import { SafeOwner } from 'src/logic/safe/store/models/safe' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn' @@ -43,12 +40,11 @@ export const OWNERS_ROW_TEST_ID = 'owners-row' const useStyles = makeStyles(styles) type Props = { - addressBook: AddressBookState granted: boolean - owners: List + owners: AddressBookState } -const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactElement => { +const ManageOwners = ({ granted, owners }: Props): React.ReactElement => { const { trackEvent } = useAnalytics() const classes = useStyles() @@ -85,8 +81,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) - const ownersWithAddressBookName = getOwnersWithNameFromAddressBook(addressBook, owners) - const ownerData = getOwnerData(ownersWithAddressBookName) + const ownerData = getOwnerData(owners) return ( <> @@ -107,7 +102,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme disablePagination label="Owners" noBorder - size={ownerData.size} + size={ownerData.length} > {(sortedData) => sortedData.map((row, index) => ( @@ -150,7 +145,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme src={ReplaceOwnerIcon} testId={REPLACE_OWNER_BTN_TEST_ID} /> - {ownerData.size > 1 && ( + {ownerData.length > 1 && ( Remove owner { const classes = useStyles() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const safeName = useSafeName(safeAddress) + const addressBookMap = useSelector(addressBookMapSelector) + const safeAddressBookEntry = addressBookMap[chainId]?.[safeAddress] + const safeName = safeAddressBookEntry?.name const defaultSafe = useSelector(defaultSafeSelector) const dispatch = useDispatch() const onRemoveSafeHandler = async () => { + // ToDo: review if this is necessary or we should directly use the `removeSafe` action. await dispatch(removeLocalSafe(safeAddress)) + // remove safe from the address book + safeAddressBookEntry && dispatch(addressBookRemove(safeAddressBookEntry)) if (sameAddress(safeAddress, defaultSafe)) { await saveDefaultSafe('') } diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index 8ff58cee..fbdcf2a9 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -1,6 +1,7 @@ import { makeStyles } from '@material-ui/core/styles' import React, { ReactElement, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { updateSafe } from '../../../../../logic/safe/store/actions/updateSafe' import { styles } from './style' @@ -16,7 +17,7 @@ import Heading from 'src/components/layout/Heading' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { addressBookAddOrUpdate } from 'src/logic/addressBook/store/actions' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications' import { sameAddress } from 'src/logic/wallets/ethAddresses' @@ -71,7 +72,9 @@ const SafeDetails = (): ReactElement => { } const handleSubmit = (values) => { - dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: safeAddress, name: values.safeName }))) + dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: safeAddress, name: values.safeName }))) + // used to trigger safe middleware and persist safe to localStorage + dispatch(updateSafe({ address: safeAddress })) const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX) dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) diff --git a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx index 61747fbc..54d6b88f 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/InfoDisplay/AddressInfo.tsx @@ -14,7 +14,7 @@ interface AddressInfoProps { } const AddressInfo = ({ address, title }: AddressInfoProps): ReactElement => { - const name = useSelector((state) => getNameFromAddressBookSelector(state, address)) + const name = useSelector((state) => getNameFromAddressBookSelector(state, { address })) const explorerUrl = getExplorerInfo(address) return ( diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index 246d269a..7d99b6f9 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -6,7 +6,6 @@ import { Button } from '@gnosis.pm/safe-react-components' import React, { ReactElement, useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import styled from 'styled-components' -import { List } from 'immutable' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' @@ -18,7 +17,6 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { SafeOwner } from 'src/logic/safe/store/models/safe' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' import { TransactionFees } from 'src/components/TransactionsFees' import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' @@ -43,14 +41,14 @@ const useStyles = makeStyles(styles) type ChangeThresholdModalProps = { onClose: () => void - owners?: List + ownersCount?: number safeAddress: string threshold?: number } export const ChangeThresholdModal = ({ onClose, - owners, + ownersCount = 0, safeAddress, threshold = 1, }: ChangeThresholdModalProps): ReactElement => { @@ -174,7 +172,7 @@ export const ChangeThresholdModal = ({ render={(props) => ( <> - {[...Array(Number(owners?.size))].map((x, index) => ( + {[...Array(Number(ownersCount))].map((x, index) => ( {index + 1} @@ -187,7 +185,7 @@ export const ChangeThresholdModal = ({ - {`out of ${owners?.size} owner(s)`} + {`out of ${ownersCount} owner(s)`} diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index 2e9246f2..ff1ab1dc 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -46,9 +46,9 @@ const ThresholdSettings = (): React.ReactElement => { Required confirmations Any transaction requires the confirmation of: - {threshold} out of {owners?.size || 0} owners + {threshold} out of {owners?.length || 0} owners - {owners && owners.size > 1 && granted && ( + {owners && owners.length > 1 && granted && (
))} @@ -77,7 +77,7 @@ export const TxOwners = ({ txDetails }: { txDetails: ExpandedTxDetails }): React {detailedExecutionInfo.executor ? 'Executed' : 'Execute'} - {detailedExecutionInfo.executor && } + {detailedExecutionInfo.executor && }
) : ( diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts b/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts index 1542d1eb..31530a0a 100644 --- a/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts +++ b/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts @@ -7,7 +7,7 @@ type AddressInfo = { name: string | undefined; image: string | undefined } type UseKnownAddressResponse = AddressInfo & { isAddressBook: boolean } export const useKnownAddress = (address: string, addressInfo: AddressInfo): UseKnownAddressResponse => { - const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, address)) + const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, { address })) const isInAddressBook = recipientName !== 'UNKNOWN' diff --git a/src/store/index.ts b/src/store/index.ts index 2552f67a..90bfd82f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,10 +2,11 @@ import { Map } from 'immutable' import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router' import { createHashHistory } from 'history' import { applyMiddleware, combineReducers, compose, createStore, CombinedState, PreloadedState, Store } from 'redux' +import { save, load } from 'redux-localstorage-simple' import thunk from 'redux-thunk' -import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware' -import addressBook, { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook' +import { addressBookMiddleware } from 'src/logic/addressBook/store/middleware' +import addressBook, { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID, @@ -30,6 +31,7 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' +import { migrateAddressBook, migrateSafeNames } from 'src/logic/addressBook/utils' import currencyValues, { CURRENCY_VALUES_KEY, CurrencyValuesState, @@ -38,11 +40,12 @@ import { currencyValuesStorageMiddleware } from 'src/logic/currencyValues/store/ export const history = createHashHistory() -// eslint-disable-next-line const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose +const abConfig = { states: [ADDRESS_BOOK_REDUCER_ID], namespace: 'gnosis_safe', namespaceSeparator: '::' } const finalCreateStore = composeEnhancers( applyMiddleware( thunk, + save(abConfig), routerMiddleware(history), notificationsMiddleware, safeStorageMiddleware, @@ -82,7 +85,15 @@ export type AppReduxState = CombinedState<{ router: RouterState }> -export const store: any = createStore(reducers, finalCreateStore) +// migrates address book before creating the store +migrateAddressBook(abConfig) + +// migrates safes +// removes the `name` key from safe object +// adds safes with name into de address book +migrateSafeNames(abConfig) + +export const store: any = createStore(reducers, load(abConfig), finalCreateStore) export const aNewStore = (localState?: PreloadedState): Store => createStore(reducers, localState, finalCreateStore) diff --git a/yarn.lock b/yarn.lock index 207135c7..b62c1a8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1393,7 +1393,7 @@ ripemd160 "^2.0.2" sha3 "^2.1.3" -"@eslint/eslintrc@^0.4.0", "@eslint/eslintrc@^0.4.1": +"@eslint/eslintrc@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.1.tgz#442763b88cecbe3ee0ec7ca6d6dd6168550cbf14" integrity sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ== @@ -1903,7 +1903,7 @@ dependencies: invariant "2" -"@ledgerhq/devices@^5.50.0", "@ledgerhq/devices@^5.51.1": +"@ledgerhq/devices@^5.51.1": version "5.51.1" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.51.1.tgz#d741a4a5d8f17c2f9d282fd27147e6fe1999edb7" integrity sha512-4w+P0VkbjzEXC7kv8T1GJ/9AVaP9I6uasMZ/JcdwZBS3qwvKo5A5z9uGhP5c7TvItzcmPb44b5Mw2kT+WjUuAA== @@ -3829,18 +3829,6 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.1.tgz#3938a5c89b27dc9a39b5de63a62ab1623ab27497" - integrity sha512-svYlHecSMCQGDO2qN1v477ax/IDQwWhc7PRBiwAdAMJE7GXk5stF4Z9R/8wbRkuX/5e9dHqbIWxjeOjckK3wLQ== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.22.1" - "@typescript-eslint/types" "4.22.1" - "@typescript-eslint/typescript-estree" "4.22.1" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - "@typescript-eslint/experimental-utils@4.23.0", "@typescript-eslint/experimental-utils@^4.0.1": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.23.0.tgz#f2059434cd6e5672bfeab2fb03b7c0a20622266f" @@ -3874,14 +3862,6 @@ "@typescript-eslint/typescript-estree" "4.23.0" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz#5bb357f94f9cd8b94e6be43dd637eb73b8f355b4" - integrity sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g== - dependencies: - "@typescript-eslint/types" "4.22.1" - "@typescript-eslint/visitor-keys" "4.22.1" - "@typescript-eslint/scope-manager@4.23.0": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz#8792ef7eacac122e2ec8fa2d30a59b8d9a1f1ce4" @@ -3895,11 +3875,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== -"@typescript-eslint/types@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.22.1.tgz#bf99c6cec0b4a23d53a61894816927f2adad856a" - integrity sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw== - "@typescript-eslint/types@4.23.0": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.23.0.tgz#da1654c8a5332f4d1645b2d9a1c64193cae3aa3b" @@ -3919,19 +3894,6 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz#dca379eead8cdfd4edc04805e83af6d148c164f9" - integrity sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A== - dependencies: - "@typescript-eslint/types" "4.22.1" - "@typescript-eslint/visitor-keys" "4.22.1" - debug "^4.1.1" - globby "^11.0.1" - is-glob "^4.0.1" - semver "^7.3.2" - tsutils "^3.17.1" - "@typescript-eslint/typescript-estree@4.23.0": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz#0753b292097523852428a6f5a1aa8ccc1aae6cd9" @@ -3952,14 +3914,6 @@ dependencies: eslint-visitor-keys "^1.1.0" -"@typescript-eslint/visitor-keys@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz#6045ae25a11662c671f90b3a403d682dfca0b7a6" - integrity sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ== - dependencies: - "@typescript-eslint/types" "4.22.1" - eslint-visitor-keys "^2.0.0" - "@typescript-eslint/visitor-keys@4.23.0": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz#7215cc977bd3b4ef22467b9023594e32f9e4e455" @@ -4714,7 +4668,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3, array.prototype.flat@^1.2.4: +array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== @@ -6976,11 +6930,6 @@ constants-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - contains-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-1.0.0.tgz#3458b332185603e8eed18f518d4a10888a3abc91" @@ -8002,14 +7951,6 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -8520,7 +8461,7 @@ errno@^0.1.3, errno@~0.1.1, errno@~0.1.7: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -8709,7 +8650,7 @@ eslint-import-resolver-node@^0.3.4: debug "^2.6.9" resolve "^1.13.1" -eslint-module-utils@^2.6.0, eslint-module-utils@^2.6.1: +eslint-module-utils@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz#b51be1e473dd0de1c5ea638e22429c2490ea8233" integrity sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A== @@ -13212,16 +13153,6 @@ listr2@^3.2.2: through "^2.3.8" wrap-ansi "^7.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -13724,6 +13655,11 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.0.tgz#91fff62458ba2eca378dd395fa85f1690bf87f60" + integrity sha512-TcuhVDV+e6X457MQAm7xIb19rWhZuEDEho7RrwxMpQ/3GhD5sDlnP188gjQQuweXHy9igdke5oUtVOXX1X8Sxg== + merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz#982ca1b5a0fde00eed2f6aeed1f9152860b8208a" @@ -14637,7 +14573,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.3: +object.values@^1.1.0, object.values@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== @@ -14998,13 +14934,6 @@ parse-headers@^2.0.0: resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.3.tgz#5e8e7512383d140ba02f0c7aa9f49b4399c92515" integrity sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA== -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -15168,13 +15097,6 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -17121,14 +17043,6 @@ read-config-file@6.0.0: json5 "^2.1.2" lazy-val "^1.0.4" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -17146,15 +17060,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -17311,6 +17216,13 @@ redux-devtools-instrument@^1.9.4: lodash "^4.17.19" symbol-observable "^1.2.0" +redux-localstorage-simple@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/redux-localstorage-simple/-/redux-localstorage-simple-2.4.0.tgz#2e016331a7e870e96c65ab3f79e5273a03faccc4" + integrity sha512-Zj28elJtO4fqXXC+gikonbKhFUkiwlalScYRn3EGUU44Pika1995AqUgzjIcsSPlBhIDV2WudFqa/YI9+3aE9Q== + dependencies: + merge "2.1.0" + redux-mock-store@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" @@ -17859,7 +17771,7 @@ rustbn.js@~0.2.0: resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca" integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== -rxjs@6, rxjs@^6.1.0, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3, rxjs@^6.6.7: +rxjs@6, rxjs@^6.1.0, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== From 51c05e928dbd5b24450d3574c5ccd834963b7ded Mon Sep 17 00:00:00 2001 From: fernandomg Date: Wed, 26 May 2021 18:47:03 -0300 Subject: [PATCH 06/20] fix runtime issue with `addressBookName` not being provided --- src/logic/addressBook/utils/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 9ba5d105..8139100e 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -39,10 +39,8 @@ export const getNameFromAddressBook = ( } export const isValidAddressBookName = (addressBookName: string): boolean => { - // TODO: this is filtering names that includes any of the keywords in the `ADDRESS_BOOK_INVALID_NAMES` - // So a name in the order of 'This is an unknown user' will be filtered too. Is this intentional? const hasInvalidName = ADDRESS_BOOK_INVALID_NAMES.find((invalidName) => - addressBookName.toUpperCase().includes(invalidName), + addressBookName?.toUpperCase().includes(invalidName), ) return !hasInvalidName } From 82519b84b63525cc2fec94d2a8f0d8b5d2a25f59 Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 27 May 2021 04:31:49 -0300 Subject: [PATCH 07/20] add back `loadedViaUrl` flag (#2337) * add back `loadedViaUrl` flag * implicitly set `loadedViaUrl` to `false` when adding a safe via form or setting its name from Settings * exclude safes whose `loadedViaUrl` flag is `true` * hide "Remove Safe" button for safes `loadedViaUrl` * add `loadedViaUrl` flag to mocked safe --- src/logic/safe/store/actions/fetchSafe.ts | 3 ++- src/logic/safe/store/models/safe.ts | 2 ++ src/logic/safe/store/selectors/index.ts | 1 + .../shouldSafeStoreBeUpdated.test.ts | 1 + .../components/Settings/SafeDetails/index.tsx | 4 ++-- src/routes/safe/components/Settings/index.tsx | 21 +++++++++++++------ 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 7e7dd7f4..622c0c46 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -20,7 +20,8 @@ import { buildSafeOwners, extractRemoteSafeInfo } from './utils' */ export const buildSafe = async (safeAddress: string): Promise => { const address = checksumAddress(safeAddress) - const safeInfo: Partial = { address } + // setting `loadedViaUrl` to false, as `buildSafe` is called on safe Load or Open flows + const safeInfo: Partial = { address, loadedViaUrl: false } const [remote, localSafeInfo] = await allSettled<[SafeInfo | null, SafeRecordProps | undefined | null]>( getSafeInfo(safeAddress), diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 8c72f58b..58f5b9a1 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -36,6 +36,7 @@ export type SafeRecordProps = { currentVersion: string needsUpdate: boolean featuresEnabled: Array + loadedViaUrl: boolean } const makeSafe = Record({ @@ -52,6 +53,7 @@ const makeSafe = Record({ currentVersion: '', needsUpdate: false, featuresEnabled: [], + loadedViaUrl: true, }) export type SafeRecord = RecordOf diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 42d7fa65..11c24303 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -32,6 +32,7 @@ export const safesListWithAddressBookNameSelector = createSelector( const addressBook = addressBookMap?.[chainId] return safesList + .filter((safeRecord) => !safeRecord.loadedViaUrl) .map((safeRecord) => { const safe = safeRecord.toObject() const name = addressBook?.[safe.address]?.name diff --git a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts index 50ccee75..17da3893 100644 --- a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts +++ b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts @@ -38,6 +38,7 @@ const getMockedOldSafe = ({ needsUpdate: needsUpdate || false, featuresEnabled: featuresEnabled || [], totalFiatBalance: '110', + loadedViaUrl: false, } } diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index ca7a3c26..434b2b63 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -73,8 +73,8 @@ const SafeDetails = (): ReactElement => { const handleSubmit = (values) => { dispatch(addressBookAddOrUpdate(makeAddressBookEntry({ address: safeAddress, name: values.safeName }))) - // used to trigger safe middleware and persist safe to localStorage - dispatch(updateSafe({ address: safeAddress })) + // setting `loadedViaUrl` to `false` as setting a safe's name is considered to intentionally add the safe + dispatch(updateSafe({ address: safeAddress, loadedViaUrl: false })) const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.SAFE_NAME_CHANGE_TX) dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 1a333223..83d317c7 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -25,7 +25,11 @@ import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import Span from 'src/components/layout/Span' -import { safeNeedsUpdateSelector, safeOwnersWithAddressBookDataSelector } from 'src/logic/safe/store/selectors' +import { + safeNeedsUpdateSelector, + safeOwnersWithAddressBookDataSelector, + safeSelector, +} from 'src/logic/safe/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab' @@ -45,6 +49,7 @@ const Settings: React.FC = () => { const owners = useSelector((state) => safeOwnersWithAddressBookDataSelector(state, chainId)) const needsUpdate = useSelector(safeNeedsUpdateSelector) const granted = useSelector(grantedSelector) + const safe = useSelector(safeSelector) const handleChange = (menuOptionIndex) => () => { setState((prevState) => ({ ...prevState, menuOptionIndex })) @@ -67,11 +72,15 @@ const Settings: React.FC = () => { ) : ( <> - - Remove Safe - Trash Icon - - + {!safe?.loadedViaUrl && ( + <> + + Remove Safe + Trash Icon + + + + )} From 2308c1f56708be6e77d7cc322e9aab24643bdd5e Mon Sep 17 00:00:00 2001 From: katspaugh Date: Thu, 27 May 2021 15:52:15 +0200 Subject: [PATCH 08/20] [Address Book v2] Fix AB v2 migration (#2345) * Refactor AB migration * Fix AB v2 and safes migration * Don't migrate if already migrated * Restore removeFromStorage --- src/logic/addressBook/utils/index.ts | 145 +----------------- src/logic/addressBook/utils/v2-migration.ts | 133 ++++++++++++++++ src/logic/exceptions/registry.ts | 1 + .../store/actions/loadSafesFromStorage.ts | 21 +-- src/logic/safe/utils/safeStorage.ts | 5 + src/store/index.ts | 19 +-- 6 files changed, 154 insertions(+), 170 deletions(-) create mode 100644 src/logic/addressBook/utils/v2-migration.ts diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 8139100e..be027db2 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -1,14 +1,9 @@ import { mustBeEthereumContractAddress } from 'src/components/forms/validator' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' -import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { SafeRecordProps } from 'src/logic/safe/store/models/safe' -import { saveSafes, StoredSafes } from 'src/logic/safe/utils' +import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { AppReduxState } from 'src/store' import { Overwrite } from 'src/types/helpers' -import { getNetworkName } from 'src/config' -import { checksumAddress } from 'src/utils/checksumAddress' -import { removeFromStorage } from 'src/utils/storage' export type OldAddressBookEntry = { address: string @@ -132,141 +127,3 @@ export const getEntryIndex = ( state.findIndex( ({ address, chainId }) => chainId === addressBookEntry.chainId && sameAddress(address, addressBookEntry.address), ) - -/** - * Migrates the safes names from the Safe Object to the Address Book - * - * Works on the persistence layer, reads from the localStorage and stores back the mutated safe info through immortalDB. - * - * @note If the Safe name is an invalid AB name, it's renamed to "Migrated from: {safe.name}" - */ -export const migrateSafeNames = ({ - states, - namespace, - namespaceSeparator, -}: { - states: string[] - namespace: string - namespaceSeparator: string -}): void => { - const PREFIX = `v2_${getNetworkName()}` - const storedSafes = localStorage.getItem(`_immortal|${PREFIX}__SAFES`) - - if (storedSafes === null) { - // nothing left to migrate - return - } - - const parsedStoredSafes = JSON.parse(storedSafes) as Record> - - if (Object.entries(parsedStoredSafes).every(([, { name }]) => name === undefined)) { - // no name key, safes already migrated - return - } - - const safesToAddressBook: AddressBookState = [] - const migratedSafes: StoredSafes = - // once removed the name from the safe object, re-create the map - Object.fromEntries( - // prepare the safe's map to iterate over it - Object.entries(parsedStoredSafes) - // exclude those safes without name - .filter(([, { name }]) => name !== undefined) - // iterate over the list of safes - .map(([safeAddress, { name, ...safe }]) => { - let safeName = name - - if (!isValidAddressBookName(name)) { - safeName = `Migrated from: ${name}` - } - - // create an entry for the AB - safesToAddressBook.push(makeAddressBookEntry({ address: safeAddress, name: safeName })) - - // return the new safe object without the name on it - return [safeAddress, safe] - }), - ) - - const [state] = states - const addressBookKey = `${namespace}${namespaceSeparator}${state}` - const storedAddressBook = localStorage.getItem(addressBookKey) - let addressBookToStore: AddressBookState = [] - - if (storedAddressBook !== null) { - // stored AB information - addressBookToStore = JSON.parse(storedAddressBook) - } - - // mutate `addressBookToStore` by adding safes' information - safesToAddressBook.forEach((entry) => { - const safeIndex = getEntryIndex(addressBookToStore, entry) - - if (safeIndex >= 0) { - // update AB entry with what was stored in the safe object - addressBookToStore[safeIndex] = entry - } else { - // add the safe entry to the AB - addressBookToStore.push(entry) - } - }) - - try { - // store the mutated address book - localStorage.setItem(addressBookKey, JSON.stringify(addressBookToStore)) - // update stored safe - saveSafes(migratedSafes).then(() => console.info('updated Safe objects')) - } catch (error) { - console.error('failed to migrate safes names into the address book', error.message) - } -} - -/** - * Migrates the AddressBook from a per-network to a global storage under the key `ADDRESS_BOOK` in `localStorage` - * - * The migrated structure will be `{ address, name, chainId }` - * - * @note Also, adds `chainId` to every entry in the AddressBook list. - */ -export const migrateAddressBook = ({ - states, - namespace, - namespaceSeparator, -}: { - states: string[] - namespace: string - namespaceSeparator: string -}): void => { - const [state] = states - const PREFIX = `v2_${getNetworkName()}` - const storedAddressBook = localStorage.getItem(`_immortal|${PREFIX}__ADDRESS_BOOK_STORAGE_KEY`) - - if (storedAddressBook === null) { - // nothing left to migrate - return - } - - let parsedAddressBook = JSON.parse(storedAddressBook) - - if (typeof parsedAddressBook === 'string') { - // double stringify? - parsedAddressBook = JSON.parse(parsedAddressBook) - } - - const migratedAddressBook = (parsedAddressBook as Omit[]) - // exclude those addresses with invalid names - .filter(({ name }) => isValidAddressBookName(name)) - .map(({ address, ...entry }) => - makeAddressBookEntry({ - address: checksumAddress(address), - ...entry, - }), - ) - - try { - localStorage.setItem(`${namespace}${namespaceSeparator}${state}`, JSON.stringify(migratedAddressBook)) - removeFromStorage('ADDRESS_BOOK_STORAGE_KEY').then(() => console.info('legacy Address Book removed')) - } catch (error) { - console.error('failed to persist the migrated address book', error.message) - } -} diff --git a/src/logic/addressBook/utils/v2-migration.ts b/src/logic/addressBook/utils/v2-migration.ts new file mode 100644 index 00000000..580f804d --- /dev/null +++ b/src/logic/addressBook/utils/v2-migration.ts @@ -0,0 +1,133 @@ +import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { saveSafes, StoredSafes } from 'src/logic/safe/utils' +import { removeFromStorage } from 'src/utils/storage' +import { getNetworkName } from 'src/config' +import { getWeb3 } from 'src/logic/wallets/getWeb3' +import { Errors, logError } from 'src/logic/exceptions/CodedException' +import { getEntryIndex, isValidAddressBookName } from '.' + +interface StorageConfig { + states: string[] + namespace: string + namespaceSeparator: string +} + +/** + * Migrates the safes names from the Safe Object to the Address Book + * + * Works on the persistence layer, reads from the localStorage and stores back the mutated safe info through immortalDB. + * + * @note If the Safe name is an invalid AB name, it's renamed to "Migrated from: {safe.name}" + */ +const migrateSafeNames = ({ states, namespace, namespaceSeparator }: StorageConfig): void => { + const prefix = `v2_${getNetworkName()}` + const safesKey = `_immortal|${prefix}__SAFES` + const storedSafes = localStorage.getItem(safesKey) + + if (!storedSafes) { + // nothing left to migrate + return + } + + const parsedStoredSafes = JSON.parse(storedSafes) as Record + + if (Object.entries(parsedStoredSafes).every(([, { name }]) => name === undefined)) { + // no name key, safes already migrated + return + } + + // make address book entries from the safe names & addresses + const safeAbEntries: AddressBookState = Object.values(parsedStoredSafes) + .filter(({ name }) => name && isValidAddressBookName(name)) + .map(({ address, name }) => makeAddressBookEntry({ address, name })) + + // remove names from the safes in place + Object.values(parsedStoredSafes).forEach((item) => { + item.owners = item.owners.map((owner: any) => owner.address) + delete item.name + }) + const migratedSafes = parsedStoredSafes as StoredSafes + + const [state] = states + const addressBookKey = `${namespace}${namespaceSeparator}${state}` + const storedAddressBook = localStorage.getItem(addressBookKey) + const addressBookToStore: AddressBookState = storedAddressBook ? JSON.parse(storedAddressBook) : [] + + // mutate `addressBookToStore` by adding safes' information + safeAbEntries.forEach((entry) => { + const safeIndex = getEntryIndex(addressBookToStore, entry) + + if (safeIndex >= 0) { + // update AB entry with what was stored in the safe object + addressBookToStore[safeIndex] = entry + } else { + // add the safe entry to the AB + addressBookToStore.push(entry) + } + }) + + // store the mutated address book + localStorage.setItem(addressBookKey, JSON.stringify(addressBookToStore)) + + // update stored safe + localStorage.setItem(safesKey, JSON.stringify(migratedSafes)) + saveSafes(migratedSafes).then(() => console.info('Safe objects migrated')) +} + +/** + * Migrates the AddressBook from a per-network to a global storage under the key `ADDRESS_BOOK` in `localStorage` + * + * The migrated structure will be `{ address, name, chainId }` + * + * @note Also, adds `chainId` to every entry in the AddressBook list. + */ +const migrateAddressBook = ({ states, namespace, namespaceSeparator }: StorageConfig): void => { + const [state] = states + const prefix = `v2_${getNetworkName()}` + const newKey = `${namespace}${namespaceSeparator}${state}` + const oldKey = 'ADDRESS_BOOK_STORAGE_KEY' + const storageKey = `_immortal|${prefix}__${oldKey}` + + if (localStorage.getItem(newKey)) { + // already migrated + return + } + + const storedAddressBook = localStorage.getItem(storageKey) + + if (!storedAddressBook) { + // nothing to migrate + return + } + + const parsedAddressBook = JSON.parse(JSON.parse(storedAddressBook as string)) + + const migratedAddressBook = (parsedAddressBook as Omit[]) + // exclude those addresses with invalid names and addresses + .filter((item) => { + return isValidAddressBookName(item.name) && getWeb3().utils.isAddress(item.address) + }) + .map(({ address, ...entry }) => + makeAddressBookEntry({ + address, + ...entry, + }), + ) + + localStorage.setItem(newKey, JSON.stringify(migratedAddressBook)) + + // Remove the old Address Book storage + localStorage.removeItem(storageKey) + removeFromStorage(oldKey).then(() => console.info('Legacy Address Book removed')) +} + +const migrate = (storageConfig: StorageConfig): void => { + try { + migrateAddressBook(storageConfig) + migrateSafeNames(storageConfig) + } catch (e) { + logError(Errors._200, e.message) + } +} + +export default migrate diff --git a/src/logic/exceptions/registry.ts b/src/logic/exceptions/registry.ts index 18c3d3b0..a9fcfc86 100644 --- a/src/logic/exceptions/registry.ts +++ b/src/logic/exceptions/registry.ts @@ -7,6 +7,7 @@ enum ErrorCodes { ___0 = '0: No such error code', _100 = '100: Invalid input in the address field', + _200 = '200: Failed migrating to the address book v2', _600 = '600: Error fetching token list', _601 = '601: Error fetching balances', } diff --git a/src/logic/safe/store/actions/loadSafesFromStorage.ts b/src/logic/safe/store/actions/loadSafesFromStorage.ts index 8aa5f560..9e0d67c7 100644 --- a/src/logic/safe/store/actions/loadSafesFromStorage.ts +++ b/src/logic/safe/store/actions/loadSafesFromStorage.ts @@ -1,24 +1,15 @@ import { Dispatch } from 'redux' - -import { SAFES_KEY } from 'src/logic/safe/utils' -import { SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { getLocalSafes } from 'src/logic/safe/utils' import { buildSafe } from 'src/logic/safe/store/reducer/safe' -import { loadFromStorage } from 'src/utils/storage' - import { addOrUpdateSafe } from './addOrUpdateSafe' const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise => { - try { - const safes = await loadFromStorage>(SAFES_KEY) + const safes = await getLocalSafes() - if (safes) { - Object.values(safes).forEach((safeProps) => { - dispatch(addOrUpdateSafe(buildSafe(safeProps))) - }) - } - } catch (err) { - // eslint-disable-next-line - console.error('Error while getting Safes from storage:', err) + if (safes) { + safes.forEach((safeProps) => { + dispatch(addOrUpdateSafe(buildSafe(safeProps))) + }) } return Promise.resolve() diff --git a/src/logic/safe/utils/safeStorage.ts b/src/logic/safe/utils/safeStorage.ts index 725cb818..157570e0 100644 --- a/src/logic/safe/utils/safeStorage.ts +++ b/src/logic/safe/utils/safeStorage.ts @@ -18,6 +18,11 @@ export const saveSafes = async (safes: StoredSafes): Promise => { } } +export const getLocalSafes = async (): Promise => { + const storedSafes = await loadStoredSafes() + return storedSafes ? Object.values(storedSafes) : undefined +} + export const getLocalSafe = async (safeAddress: string): Promise => { const storedSafes = await loadStoredSafes() return storedSafes?.[safeAddress] diff --git a/src/store/index.ts b/src/store/index.ts index 90bfd82f..453137d1 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -31,7 +31,7 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' -import { migrateAddressBook, migrateSafeNames } from 'src/logic/addressBook/utils' +import migrateAddressBook from 'src/logic/addressBook/utils/v2-migration' import currencyValues, { CURRENCY_VALUES_KEY, CurrencyValuesState, @@ -41,11 +41,13 @@ import { currencyValuesStorageMiddleware } from 'src/logic/currencyValues/store/ export const history = createHashHistory() const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose -const abConfig = { states: [ADDRESS_BOOK_REDUCER_ID], namespace: 'gnosis_safe', namespaceSeparator: '::' } + +const localStorageConfig = { states: [ADDRESS_BOOK_REDUCER_ID], namespace: 'SAFE', namespaceSeparator: '__' } + const finalCreateStore = composeEnhancers( applyMiddleware( thunk, - save(abConfig), + save(localStorageConfig), routerMiddleware(history), notificationsMiddleware, safeStorageMiddleware, @@ -85,15 +87,10 @@ export type AppReduxState = CombinedState<{ router: RouterState }> -// migrates address book before creating the store -migrateAddressBook(abConfig) +// Address Book v2 migration +migrateAddressBook(localStorageConfig) -// migrates safes -// removes the `name` key from safe object -// adds safes with name into de address book -migrateSafeNames(abConfig) - -export const store: any = createStore(reducers, load(abConfig), finalCreateStore) +export const store: any = createStore(reducers, load(localStorageConfig), finalCreateStore) export const aNewStore = (localState?: PreloadedState): Store => createStore(reducers, localState, finalCreateStore) From 0d915a6bc1629b797d2df3d6d32291925c04c557 Mon Sep 17 00:00:00 2001 From: Mati Dastugue Date: Thu, 27 May 2021 12:26:38 -0300 Subject: [PATCH 09/20] Export - Add loading state when click on download button (#2325) * Add loading state when click on download button * Expand clickable area for link --- .../AddressBook/ExportEntriesModal/index.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx index 143d107f..487272cb 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx @@ -31,6 +31,14 @@ const ImageContainer = styled(Row)` padding: ${md} ${lg}; justify-content: center; ` +const StyledButton = styled(Button)` + &.MuiButtonBase-root.MuiButton-root { + padding: 0; + .MuiButton-label { + height: 100%; + } + } +` const InfoContainer = styled(Row)` background-color: ${background}; @@ -47,9 +55,11 @@ const StyledLoader = styled(Loader)` margin-right: 5px; ` const StyledCSVLink = styled(CSVDownloader)` + height: 100%; display: flex; flex: 1; justify-content: center; + align-items: center; ` const StyledIcon = styled(Icon)` @@ -70,8 +80,9 @@ export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps) const date = format(new Date(), 'yyyy-MM-dd') - const handleClose = () => + const handleClose = () => { //This timeout prevents modal to be closed abruptly + setLoading(true) setTimeout(() => { if (!loading) { const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_EXPORT_ENTRIES) @@ -82,6 +93,7 @@ export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps) } onClose() }, 600) + } useEffect(() => { const handleCsvData = () => { @@ -147,7 +159,12 @@ export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps) - +
From e58b6d6b7b4e44e38205e7f84055e51f76330a21 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Tue, 1 Jun 2021 12:17:58 +0200 Subject: [PATCH 10/20] [Address Book v2] 1603 Use addressbook over known addresses (#2352) * Use ADDRESS_BOOK_DEFAULT_NAME instead of hard coded string * Use address book name in Send Collectible review step * Show name if known in contract interaction review * Use string comparison util --- .../AddressBook/EllipsisTransactionDetails/index.tsx | 11 ++++++++--- .../screens/ContractInteraction/Review/index.tsx | 12 +++++++++++- .../SendModal/screens/ReviewCollectible/index.tsx | 2 ++ .../SendModal/screens/SendCollectible/index.tsx | 3 ++- .../SpendingLimit/InfoDisplay/AddressInfo.tsx | 3 ++- .../safe/components/Transactions/TxList/OwnerRow.tsx | 4 +++- .../components/Transactions/TxList/TxInfoDetails.tsx | 6 ------ .../Transactions/TxList/hooks/useKnownAddress.ts | 6 ++++-- 8 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx b/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx index c9a3a386..b7dd5250 100644 --- a/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx +++ b/src/routes/safe/components/AddressBook/EllipsisTransactionDetails/index.tsx @@ -7,6 +7,9 @@ import { push } from 'connected-react-router' import React from 'react' import { useDispatch, useSelector } from 'react-redux' +import { sameString } from 'src/utils/strings' +import { ADDRESS_BOOK_DEFAULT_NAME } from 'src/logic/addressBook/model/addressBook' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' import { SAFELIST_ADDRESS } from 'src/routes/routes' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { xs } from 'src/theme/variables' @@ -35,13 +38,11 @@ const useStyles = makeStyles( type EllipsisTransactionDetailsProps = { address: string - knownAddress?: boolean sendModalOpenHandler?: () => void } export const EllipsisTransactionDetails = ({ address, - knownAddress, sendModalOpenHandler, }: EllipsisTransactionDetailsProps): React.ReactElement => { const classes = useStyles() @@ -51,6 +52,10 @@ export const EllipsisTransactionDetails = ({ const currentSafeAddress = useSelector(safeParamAddressFromStateSelector) const isOwnerConnected = useSelector(grantedSelector) + const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, { address })) + // We have to check that the name returned is not UNKNOWN + const isStoredInAddressBook = !sameString(recipientName, ADDRESS_BOOK_DEFAULT_NAME) + const handleClick = (event) => setAnchorEl(event.currentTarget) const closeMenuHandler = () => setAnchorEl(null) @@ -73,7 +78,7 @@ export const EllipsisTransactionDetails = ({ , ] : null} - {knownAddress ? ( + {isStoredInAddressBook ? ( Edit Address book Entry ) : ( Add to address book diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index 37205482..1609b8da 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -27,6 +27,7 @@ import { getValueFromTxInputs, } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils' import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus' import { ButtonStatus, Modal } from 'src/components/Modal' import { TransactionFees } from 'src/components/TransactionsFees' @@ -60,6 +61,9 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE const [manualSafeTxGas, setManualSafeTxGas] = useState(0) const [manualGasPrice, setManualGasPrice] = useState() const [manualGasLimit, setManualGasLimit] = useState() + const addressName = useSelector((state) => + getNameFromAddressBookSelector(state, { address: tx.contractAddress as string }), + ) const [txInfo, setTxInfo] = useState<{ txRecipient: string @@ -154,7 +158,13 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE - + diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index 29018978..6a4237cf 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -35,6 +35,7 @@ const useStyles = makeStyles(styles) export type CollectibleTx = { recipientAddress: string + recipientName?: string assetAddress: string assetName: string nftTokenId: string @@ -177,6 +178,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = { { const ownerName = useSelector((state) => getNameFromAddressBookSelector(state, { address })) @@ -11,7 +13,7 @@ export const OwnerRow = ({ address }: { address: string }): ReactElement => { return ( { - const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, { address })) - const knownAddress = recipientName !== 'UNKNOWN' - const { txLocation } = useContext(TxLocationContext) const canRepeatTransaction = // is transfer type by context @@ -89,7 +84,6 @@ export const TxInfoDetails = ({ diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts b/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts index 31530a0a..1a08832b 100644 --- a/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts +++ b/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts @@ -1,5 +1,7 @@ import { useSelector } from 'react-redux' +import { sameString } from 'src/utils/strings' +import { ADDRESS_BOOK_DEFAULT_NAME } from 'src/logic/addressBook/model/addressBook' import { getNameFromAddressBookSelector } from 'src/logic/addressBook/store/selectors' type AddressInfo = { name: string | undefined; image: string | undefined } @@ -8,8 +10,8 @@ type UseKnownAddressResponse = AddressInfo & { isAddressBook: boolean } export const useKnownAddress = (address: string, addressInfo: AddressInfo): UseKnownAddressResponse => { const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, { address })) - - const isInAddressBook = recipientName !== 'UNKNOWN' + // We have to check that the name returned is not UNKNOWN + const isInAddressBook = !sameString(recipientName, ADDRESS_BOOK_DEFAULT_NAME) return isInAddressBook ? { From fc0c450a7467b806430e6853deb8db221c84d4cc Mon Sep 17 00:00:00 2001 From: Fernando Date: Tue, 1 Jun 2021 09:13:18 -0300 Subject: [PATCH 11/20] [AddressBook v2] - Avoid using `checksumAddress` inside reducer (#2355) --- src/components/forms/validator.ts | 5 +- src/logic/addressBook/store/reducer/index.ts | 18 ++--- src/routes/load/container/Load.tsx | 3 +- src/routes/open/container/Open.tsx | 2 +- .../AddressBook/ImportEntryModal/index.tsx | 5 +- .../safe/components/AddressBook/index.tsx | 10 +-- .../ManageOwners/EditOwnerModal/index.tsx | 16 ++--- .../ManageOwners/RemoveOwnerModal/index.tsx | 26 +++---- .../screens/CheckOwner/index.tsx | 12 ++-- .../RemoveOwnerModal/screens/Review/index.tsx | 31 ++++---- .../ManageOwners/ReplaceOwnerModal/index.tsx | 71 ++++++------------- .../screens/OwnerForm/index.tsx | 22 +++--- .../screens/Review/index.tsx | 46 ++++++------ .../Settings/ManageOwners/dataFetcher.ts | 4 +- .../Settings/ManageOwners/index.tsx | 52 +++++++------- src/utils/__tests__/checksumAddress.test.ts | 20 ++++++ src/utils/__tests__/isValidAddress.test.ts | 19 +++++ src/utils/checksumAddress.ts | 12 +++- src/utils/isValidAddress.ts | 11 +++ 19 files changed, 192 insertions(+), 193 deletions(-) create mode 100644 src/utils/__tests__/checksumAddress.test.ts create mode 100644 src/utils/__tests__/isValidAddress.test.ts create mode 100644 src/utils/isValidAddress.ts 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 +} From 94bee4f8df6105fc593acf11b392425ea0733270 Mon Sep 17 00:00:00 2001 From: Fernando Date: Tue, 1 Jun 2021 09:49:51 -0300 Subject: [PATCH 12/20] [Address Book v2] - Prevent name validation in AB reducer (#2356) Co-authored-by: katspaugh --- src/components/forms/validator.test.ts | 23 ++++++++++++++ src/components/forms/validator.ts | 13 ++++++++ src/logic/addressBook/store/reducer/index.ts | 30 +++++++------------ src/logic/addressBook/utils/index.ts | 2 +- .../load/components/DetailsForm/index.tsx | 4 +-- .../open/components/SafeNameForm/index.tsx | 4 +-- .../CreateEditEntryModal/index.tsx | 8 ++--- .../AddOwnerModal/screens/OwnerForm/index.tsx | 4 +-- .../ManageOwners/EditOwnerModal/index.tsx | 4 +-- .../screens/OwnerForm/index.tsx | 4 +-- .../components/Settings/SafeDetails/index.tsx | 4 +-- 11 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/components/forms/validator.test.ts b/src/components/forms/validator.test.ts index 533e4c23..a8c47d66 100644 --- a/src/components/forms/validator.test.ts +++ b/src/components/forms/validator.test.ts @@ -14,6 +14,7 @@ import { addressIsNotCurrentSafe, OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR, mustBeHexData, + validAddressBookName, } from 'src/components/forms/validator' describe('Forms > Validators', () => { @@ -249,4 +250,26 @@ describe('Forms > Validators', () => { expect(differentFrom('a')('a')).toEqual(getDifferentFromErrMsg('a')) }) }) + + describe('validAddressBookName validator', () => { + it('Returns error for an empty string', () => { + expect(validAddressBookName('')).toBe('Should be 1 to 50 symbols') + }) + it('Returns error for a name longer than 50 chars', () => { + expect(validAddressBookName('abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabc')).toBe( + 'Should be 1 to 50 symbols', + ) + }) + it('Returns error for a blacklisted name', () => { + const blacklistedErrorMessage = 'Name should not include: UNKNOWN, OWNER #, MY WALLET' + + expect(validAddressBookName('unknown')).toBe(blacklistedErrorMessage) + expect(validAddressBookName('unknown a')).toBe(blacklistedErrorMessage) + expect(validAddressBookName('owner #1')).toBe(blacklistedErrorMessage) + expect(validAddressBookName('My Wallet')).toBe(blacklistedErrorMessage) + }) + it('Returns undefined for a non-blacklisted name', () => { + expect(validAddressBookName('A valid name')).toBeUndefined() + }) + }) }) diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 90ac6cde..5040e352 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -5,6 +5,7 @@ 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' +import { ADDRESS_BOOK_INVALID_NAMES, isValidAddressBookName } from 'src/logic/addressBook/utils' type ValidatorReturnType = string | undefined export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType @@ -134,3 +135,15 @@ export const differentFrom = (diffValue: number | string) => (value: string): Va } export const noErrorsOn = (name: string, errors: Record): boolean => errors[name] === undefined + +export const validAddressBookName = (name: string): string | undefined => { + const lengthError = minMaxLength(1, 50)(name) + + if (lengthError === undefined) { + return isValidAddressBookName(name) + ? undefined + : `Name should not include: ${ADDRESS_BOOK_INVALID_NAMES.join(', ')}` + } + + return lengthError +} diff --git a/src/logic/addressBook/store/reducer/index.ts b/src/logic/addressBook/store/reducer/index.ts index 759b8a19..5b1259d5 100644 --- a/src/logic/addressBook/store/reducer/index.ts +++ b/src/logic/addressBook/store/reducer/index.ts @@ -2,7 +2,7 @@ import { Action, handleActions } from 'redux-actions' import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions' -import { getEntryIndex, isValidAddressBookName } from 'src/logic/addressBook/utils' +import { getEntryIndex } from 'src/logic/addressBook/utils' import { AppReduxState } from 'src/store' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' @@ -15,11 +15,6 @@ export default handleActions( const newState = [...state] const addressBookEntry = action.payload - if (!isValidAddressBookName(addressBookEntry.name)) { - // prevent adding an invalid name - return newState - } - const entryIndex = getEntryIndex(newState, addressBookEntry) // update @@ -48,20 +43,17 @@ export default handleActions( const newState = [...state] const addressBookEntries = action.payload - addressBookEntries - // exclude those entries with invalid name - .filter(({ name }) => isValidAddressBookName(name)) - .forEach((addressBookEntry) => { - const entryIndex = getEntryIndex(newState, addressBookEntry) + addressBookEntries.forEach((addressBookEntry) => { + const entryIndex = getEntryIndex(newState, addressBookEntry) - if (entryIndex >= 0) { - // update - newState[entryIndex] = addressBookEntry - } else { - // add - newState.push(addressBookEntry) - } - }) + if (entryIndex >= 0) { + // update + newState[entryIndex] = addressBookEntry + } else { + // add + newState.push(addressBookEntry) + } + }) return newState }, diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index be027db2..c08e462c 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -15,7 +15,7 @@ export type OldAddressBookType = { [safeAddress: string]: [OldAddressBookEntry] } -const ADDRESS_BOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET', ''] +export const ADDRESS_BOOK_INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET'] type GetNameFromAddressBookOptions = { filterOnlyValidName: boolean diff --git a/src/routes/load/components/DetailsForm/index.tsx b/src/routes/load/components/DetailsForm/index.tsx index e3b49176..4adb2dbc 100644 --- a/src/routes/load/components/DetailsForm/index.tsx +++ b/src/routes/load/components/DetailsForm/index.tsx @@ -15,7 +15,7 @@ import { noErrorsOn, required, composeValidators, - minMaxLength, + validAddressBookName, } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' @@ -95,7 +95,7 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => placeholder="Name of the Safe*" text="Safe name" type="text" - validate={composeValidators(required, minMaxLength(1, 50))} + validate={composeValidators(required, validAddressBookName)} testId="load-safe-name-field" /> diff --git a/src/routes/open/components/SafeNameForm/index.tsx b/src/routes/open/components/SafeNameForm/index.tsx index 67240bdf..d8a29619 100644 --- a/src/routes/open/components/SafeNameForm/index.tsx +++ b/src/routes/open/components/SafeNameForm/index.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components' import OpenPaper from 'src/components/Stepper/OpenPaper' import Field from 'src/components/forms/Field' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' +import { composeValidators, required, validAddressBookName } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Paragraph from 'src/components/layout/Paragraph' import { FIELD_NAME } from 'src/routes/open/components/fields' @@ -56,7 +56,7 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => placeholder="Name of the new Safe" text="Safe name" type="text" - validate={composeValidators(required, minMaxLength(1, 50))} + validate={composeValidators(required, validAddressBookName)} testId="create-safe-name-field" /> diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx index 3af9c40c..fe3bc4dd 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -9,7 +9,7 @@ import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/components/forms/validator' +import { composeValidators, required, uniqueAddress, validAddressBookName } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' @@ -90,11 +90,11 @@ export const CreateEditEntryModal = ({ diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx index 71e98a78..d6bbada5 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx @@ -17,9 +17,9 @@ import TextField from 'src/components/forms/TextField' import { addressIsNotCurrentSafe, composeValidators, - minMaxLength, required, uniqueAddress, + validAddressBookName, } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' @@ -118,7 +118,7 @@ export const OwnerForm = ({ onClose, onSubmit, initialValues }: OwnerFormProps): testId={ADD_OWNER_NAME_INPUT_TEST_ID} text="Owner name*" type="text" - validate={composeValidators(required, minMaxLength(1, 50))} + validate={composeValidators(required, validAddressBookName)} /> {async (address: string) => { diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index a423e420..9caed1ea 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' +import { composeValidators, required, validAddressBookName } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' @@ -76,7 +76,7 @@ export const EditOwnerModal = ({ isOpen, onClose, owner }: OwnProps): React.Reac testId={RENAME_OWNER_INPUT_TEST_ID} text="Owner name*" type="text" - validate={composeValidators(required, minMaxLength(1, 50))} + validate={composeValidators(required, validAddressBookName)} /> 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 1de744cb..0ed8f358 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 @@ -12,9 +12,9 @@ import TextField from 'src/components/forms/TextField' import { addressIsNotCurrentSafe, composeValidators, - minMaxLength, required, uniqueAddress, + validAddressBookName, } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' @@ -146,7 +146,7 @@ export const OwnerForm = ({ onClose, onSubmit, owner, initialValues }: OwnerForm testId={REPLACE_OWNER_NAME_INPUT_TEST_ID} text="Owner name*" type="text" - validate={composeValidators(required, minMaxLength(1, 50))} + validate={composeValidators(required, validAddressBookName)} /> {async (address: string) => { diff --git a/src/routes/safe/components/Settings/SafeDetails/index.tsx b/src/routes/safe/components/Settings/SafeDetails/index.tsx index 434b2b63..fbc90e72 100644 --- a/src/routes/safe/components/Settings/SafeDetails/index.tsx +++ b/src/routes/safe/components/Settings/SafeDetails/index.tsx @@ -10,7 +10,7 @@ import Modal from 'src/components/Modal' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' +import { composeValidators, required, validAddressBookName } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' @@ -166,7 +166,7 @@ const SafeDetails = (): ReactElement => { testId={SAFE_NAME_INPUT_TEST_ID} text="Safe name*" type="text" - validate={composeValidators(required, minMaxLength(1, 50))} + validate={composeValidators(required, validAddressBookName)} /> From 9242d80079bbe2d72fab411c3dd069407551fd82 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 2 Jun 2021 14:36:26 +0200 Subject: [PATCH 13/20] Use the same link in the import and export modals (#2380) --- .../AddressBook/ExportEntriesModal/index.tsx | 23 +++------------- .../components/AddressBook/HelpInfo/index.tsx | 27 +++++++++++++++++++ .../AddressBook/ImportEntryModal/index.tsx | 24 +++-------------- 3 files changed, 34 insertions(+), 40 deletions(-) create mode 100644 src/routes/safe/components/AddressBook/HelpInfo/index.tsx diff --git a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx index 487272cb..32535f6e 100644 --- a/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ExportEntriesModal/index.tsx @@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useState } from 'react' import { format } from 'date-fns' import { useSelector, useDispatch } from 'react-redux' import { CSVDownloader, jsonToCSV } from 'react-papaparse' -import { Button, Icon, Link, Loader, Text } from '@gnosis.pm/safe-react-components' +import { Button, Loader, Text } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications' @@ -17,6 +17,7 @@ import { lg, md, background } from 'src/theme/variables' import { Modal } from 'src/components/Modal' import Img from 'src/components/layout/Img' import Row from 'src/components/layout/Row' +import HelpInfo from 'src/routes/safe/components/AddressBook/HelpInfo' import SuccessSvg from './assets/success.svg' import ErrorSvg from './assets/error.svg' @@ -62,14 +63,6 @@ const StyledCSVLink = styled(CSVDownloader)` align-items: center; ` -const StyledIcon = styled(Icon)` - svg { - position: relative; - top: 4px; - left: 4px; - } -` - export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps): ReactElement => { const dispatch = useDispatch() const addressBook: AddressBookState = useSelector(addressBookSelector) @@ -132,17 +125,7 @@ export const ExportEntriesModal = ({ isOpen, onClose }: ExportEntriesModalProps) You're about to export a CSV file with{' '} {addressBook.length} address book entries.
- - - Learn more about importing / exporting an address book. - - - +
. diff --git a/src/routes/safe/components/AddressBook/HelpInfo/index.tsx b/src/routes/safe/components/AddressBook/HelpInfo/index.tsx new file mode 100644 index 00000000..b90ee141 --- /dev/null +++ b/src/routes/safe/components/AddressBook/HelpInfo/index.tsx @@ -0,0 +1,27 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' +import { Text, Link, Icon } from '@gnosis.pm/safe-react-components' + +const StyledIcon = styled(Icon)` + svg { + position: relative; + top: 4px; + left: 4px; + } +` + +const HelpInfo = (): ReactElement => ( + + + Learn about the address book import and export + + + +) + +export default HelpInfo diff --git a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx b/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx index 98b0870a..23ec0f28 100644 --- a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx @@ -1,12 +1,13 @@ import React, { useState } from 'react' import styled from 'styled-components' -import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' +import { Text } from '@gnosis.pm/safe-react-components' 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' +import HelpInfo from 'src/routes/safe/components/AddressBook/HelpInfo' const ImportContainer = styled.div` flex-direction: column; @@ -26,13 +27,6 @@ const InfoContainer = styled.div` text-align: center; margin-top: 16px; ` -const StyledIcon = styled(Icon)` - svg { - position: relative; - top: 4px; - left: 4px; - } -` const WRONG_FILE_EXTENSION_ERROR = 'Only CSV files are allowed' const FILE_SIZE_TOO_BIG = 'The size of the file is over 1 MB' @@ -187,18 +181,8 @@ const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => { )} {!csvLoaded && importError === '' && ( - Only CSV files are allowed in the format [Address, Name] separated by comma.
- - - Learn more about importing / exporting an address book. - - - + Only CSV files exported from Gnosis Safe are allowed.
+
)} {csvLoaded && importError === '' && ( From 5bc38ff09e0796b7fca9fdd1deccc816eded565c Mon Sep 17 00:00:00 2001 From: Fernando Date: Wed, 2 Jun 2021 10:55:13 -0300 Subject: [PATCH 14/20] [Address Book v2] - Avoid deleting addressBook entry when deleting a safe (#2374) * avoid deleting addressBook entry when deleting a safe * avoid deleting addressBook entry when deleting a safe --- src/routes/safe/components/Settings/RemoveSafeModal/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx index 651017ac..47b31d14 100644 --- a/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/RemoveSafeModal/index.tsx @@ -11,7 +11,6 @@ import Block from 'src/components/layout/Block' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { addressBookRemove } from 'src/logic/addressBook/store/actions' import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors' import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { WELCOME_ADDRESS } from 'src/routes/routes' @@ -41,8 +40,7 @@ export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): Reac const onRemoveSafeHandler = async () => { // ToDo: review if this is necessary or we should directly use the `removeSafe` action. await dispatch(removeLocalSafe(safeAddress)) - // remove safe from the address book - safeAddressBookEntry && dispatch(addressBookRemove(safeAddressBookEntry)) + if (sameAddress(safeAddress, defaultSafe)) { await saveDefaultSafe('') } From c5207f75c71941c089b7c73a88efac19abfd44c1 Mon Sep 17 00:00:00 2001 From: juampibermani <30930241+juampibermani@users.noreply.github.com> Date: Wed, 2 Jun 2021 12:13:40 -0300 Subject: [PATCH 15/20] Feature/2317 update import export global ab (#2370) * Verify chainId column and called import reducer * Added dispatch for addressbook import * Fixed store reducer and notification * Validate all entries on csv data validation * Using utils to verify imported addresses * Add props typing to ImportEntriesModal * Convert chainId to number --- .../addressBook/store/middleware/index.ts | 6 +-- src/logic/addressBook/store/reducer/index.ts | 46 +++++++++---------- .../notifications/notificationBuilder.tsx | 15 ++++++ src/logic/notifications/notificationTypes.ts | 5 ++ .../safe/transactions/notifiedTransactions.ts | 1 + .../index.tsx | 30 +++++++----- .../safe/components/AddressBook/index.tsx | 10 ++-- 7 files changed, 70 insertions(+), 43 deletions(-) rename src/routes/safe/components/AddressBook/{ImportEntryModal => ImportEntriesModal}/index.tsx (87%) diff --git a/src/logic/addressBook/store/middleware/index.ts b/src/logic/addressBook/store/middleware/index.ts index a1d12d02..3f37222a 100644 --- a/src/logic/addressBook/store/middleware/index.ts +++ b/src/logic/addressBook/store/middleware/index.ts @@ -3,14 +3,12 @@ import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -const watchedActions = [Object.keys(ADDRESS_BOOK_ACTIONS)] +const watchedActions = Object.values(ADDRESS_BOOK_ACTIONS) export const addressBookMiddleware = (store) => (next) => async (action) => { const handledAction = next(action) - if (watchedActions.includes(action.type)) { const { dispatch } = store - switch (action.type) { case ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE: { const { shouldAvoidUpdatesNotifications } = action.payload @@ -26,6 +24,8 @@ export const addressBookMiddleware = (store) => (next) => async (action) => { break } case ADDRESS_BOOK_ACTIONS.IMPORT: { + const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESS_BOOK_IMPORT_ENTRIES) + dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) break } default: diff --git a/src/logic/addressBook/store/reducer/index.ts b/src/logic/addressBook/store/reducer/index.ts index 5b1259d5..a2554cf1 100644 --- a/src/logic/addressBook/store/reducer/index.ts +++ b/src/logic/addressBook/store/reducer/index.ts @@ -2,13 +2,33 @@ import { Action, handleActions } from 'redux-actions' import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' import { ADDRESS_BOOK_ACTIONS } from 'src/logic/addressBook/store/actions' -import { getEntryIndex } from 'src/logic/addressBook/utils' +import { getEntryIndex, isValidAddressBookName } from 'src/logic/addressBook/utils' import { AppReduxState } from 'src/store' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' type Payloads = AddressBookEntry | AddressBookState +const batchLoadEntries = (state, action: Action): AddressBookState => { + const newState = [...state] + const addressBookEntries = action.payload + addressBookEntries + // exclude those entries with invalid name + .filter(({ name }) => isValidAddressBookName(name)) + .forEach((addressBookEntry) => { + const entryIndex = getEntryIndex(newState, addressBookEntry) + + if (entryIndex >= 0) { + // update + newState[entryIndex] = addressBookEntry + } else { + // add + newState.push(addressBookEntry) + } + }) + + return newState +} export default handleActions( { [ADDRESS_BOOK_ACTIONS.ADD_OR_UPDATE]: (state, action: Action) => { @@ -39,28 +59,8 @@ export default handleActions( return newState }, - [ADDRESS_BOOK_ACTIONS.SAFE_LOAD]: (state, action: Action) => { - const newState = [...state] - const addressBookEntries = action.payload - - addressBookEntries.forEach((addressBookEntry) => { - const entryIndex = getEntryIndex(newState, addressBookEntry) - - if (entryIndex >= 0) { - // update - newState[entryIndex] = addressBookEntry - } else { - // add - newState.push(addressBookEntry) - } - }) - - return newState - }, - [ADDRESS_BOOK_ACTIONS.IMPORT](...args) { - // same functionality, but `IMPORT` will trigger notifications when called - return this[ADDRESS_BOOK_ACTIONS.SAFE_LOAD](...args) - }, + [ADDRESS_BOOK_ACTIONS.SAFE_LOAD]: batchLoadEntries, + [ADDRESS_BOOK_ACTIONS.IMPORT]: batchLoadEntries, }, [], ) diff --git a/src/logic/notifications/notificationBuilder.tsx b/src/logic/notifications/notificationBuilder.tsx index 28742984..ff70ee4e 100644 --- a/src/logic/notifications/notificationBuilder.tsx +++ b/src/logic/notifications/notificationBuilder.tsx @@ -142,6 +142,17 @@ const addressBookEditEntry = { afterExecutionError: null, } +const addressBookImportEntries = { + beforeExecution: null, + afterRejection: null, + waitingConfirmation: null, + afterExecution: { + noMoreConfirmationsNeeded: NOTIFICATIONS.ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS, + moreConfirmationsNeeded: null, + }, + afterExecutionError: null, +} + const addressBookDeleteEntry = { beforeExecution: null, afterRejection: null, @@ -212,6 +223,10 @@ export const getNotificationsFromTxType: any = (txType, origin) => { notificationsQueue = addressBookEditEntry break } + case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_IMPORT_ENTRIES: { + notificationsQueue = addressBookImportEntries + break + } case TX_NOTIFICATION_TYPES.ADDRESS_BOOK_DELETE_ENTRY: { notificationsQueue = addressBookDeleteEntry break diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index 8597c919..b83c08b8 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -52,6 +52,7 @@ const NOTIFICATION_IDS = { WRONG_NETWORK_MSG: 'WRONG_NETWORK_MSG', ADDRESS_BOOK_NEW_ENTRY_SUCCESS: 'ADDRESS_BOOK_NEW_ENTRY_SUCCESS', ADDRESS_BOOK_EDIT_ENTRY_SUCCESS: 'ADDRESS_BOOK_EDIT_ENTRY_SUCCESS', + ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS: 'ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS', ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: 'ADDRESS_BOOK_DELETE_ENTRY_SUCCESS', ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS: 'ADDRESS_BOOK_EXPORT_ENTRIES_SUCCESS', ADDRESS_BOOK_EXPORT_ENTRIES_ERROR: 'ADDRESS_BOOK_EXPORT_ENTRIES_ERROR', @@ -209,6 +210,10 @@ export const NOTIFICATIONS: Record = { message: 'Entry saved successfully', options: { variant: SUCCESS, persist: false, preventDuplicate: false }, }, + ADDRESS_BOOK_IMPORT_ENTRIES_SUCCESS: { + message: 'Entries imported successfully', + options: { variant: SUCCESS, persist: false, preventDuplicate: false }, + }, ADDRESS_BOOK_DELETE_ENTRY_SUCCESS: { message: 'Entry deleted successfully', options: { variant: SUCCESS, persist: false, preventDuplicate: false }, diff --git a/src/logic/safe/transactions/notifiedTransactions.ts b/src/logic/safe/transactions/notifiedTransactions.ts index a71d9d14..9fc96e79 100644 --- a/src/logic/safe/transactions/notifiedTransactions.ts +++ b/src/logic/safe/transactions/notifiedTransactions.ts @@ -12,4 +12,5 @@ export const TX_NOTIFICATION_TYPES = { ADDRESS_BOOK_EDIT_ENTRY: 'ADDRESS_BOOK_EDIT_ENTRY', ADDRESS_BOOK_DELETE_ENTRY: 'ADDRESS_BOOK_DELETE_ENTRY', ADDRESS_BOOK_EXPORT_ENTRIES: 'ADDRESS_BOOK_EXPORT_ENTRIES', + ADDRESS_BOOK_IMPORT_ENTRIES: 'ADDRESS_BOOK_IMPORT_ENTRIES', } diff --git a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx b/src/routes/safe/components/AddressBook/ImportEntriesModal/index.tsx similarity index 87% rename from src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx rename to src/routes/safe/components/AddressBook/ImportEntriesModal/index.tsx index 23ec0f28..2c07c7d0 100644 --- a/src/routes/safe/components/AddressBook/ImportEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/ImportEntriesModal/index.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react' +import React, { ReactElement, useState } from 'react' import styled from 'styled-components' import { Text } from '@gnosis.pm/safe-react-components' 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 { isValidAddress } from 'src/utils/isValidAddress' import { checksumAddress } from 'src/utils/checksumAddress' import HelpInfo from 'src/routes/safe/components/AddressBook/HelpInfo' @@ -37,7 +37,13 @@ const IMPORT_SUPPORTED_FORMATS = [ 'text/csv', ] -const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => { +type ImportEntriesModalProps = { + importEntryModalHandler: (addressList: AddressBookEntry[]) => void + isOpen: boolean + onClose: () => void +} + +const ImportEntriesModal = ({ importEntryModalHandler, isOpen, onClose }: ImportEntriesModalProps): ReactElement => { const [csvLoaded, setCsvLoaded] = useState(false) const [importError, setImportError] = useState('') const [entryList, setEntryList] = useState([]) @@ -63,8 +69,7 @@ const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => { } const formatedList = slicedData.map((entry) => { - const address = entry.data[0] - return { address: checksumAddress(address), name: entry.data[1] } + return { address: checksumAddress(entry.data[0]), name: entry.data[1], chainId: parseInt(entry.data[2]) } }) setEntryList(formatedList) setImportError('') @@ -86,16 +91,19 @@ const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => { const validateCsvData = (data) => { for (let index = 0; index < data.length; index++) { const entry = data[index] - if (!entry.data[0] || !entry.data[1]) { - return `Invalid amount of columns on row ${index + 2}` + if (!entry.data[0] || !entry.data[1] || !entry.data[2]) { + return `Invalid amount of columns on row ${index + 1}` } // Verify address properties const address = entry.data[0].toLowerCase() - if (!getWeb3().utils.isAddress(address)) { - return `Invalid address on row ${index + 2}` + if (!isValidAddress(address)) { + return `Invalid address on row ${index + 1}` + } + if (isNaN(entry.data[2])) { + return `Invalid chain id on row ${index + 1}` } - return } + return } const handleOnError = (error) => { @@ -208,4 +216,4 @@ const ImportEntryModal = ({ importEntryModalHandler, isOpen, onClose }) => { ) } -export default ImportEntryModal +export default ImportEntriesModal diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 1f90ab92..9672e7e2 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -17,7 +17,7 @@ import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { addressBookAddOrUpdate, addressBookRemove } from 'src/logic/addressBook/store/actions' +import { addressBookAddOrUpdate, addressBookImport, addressBookRemove } from 'src/logic/addressBook/store/actions' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses' import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal' @@ -34,7 +34,7 @@ import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/saf import { checksumAddress } from 'src/utils/checksumAddress' import { grantedSelector } from 'src/routes/safe/container/selector' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' -import ImportEntryModal from './ImportEntryModal' +import ImportEntriesModal from './ImportEntriesModal' const StyledButton = styled(Button)` &&.MuiButton-root { @@ -148,9 +148,7 @@ const AddressBookTable = (): ReactElement => { } const importEntryModalHandler = (addressList: AddressBookEntry[]) => { - addressList.forEach((entry) => { - dispatch(addressBookAddOrUpdate(makeAddressBookEntry(entry))) - }) + dispatch(addressBookImport(addressList)) setImportEntryModalOpen(false) } @@ -307,7 +305,7 @@ const AddressBookTable = (): ReactElement => { onClose={() => setDeleteEntryModalOpen(false)} /> setExportEntriesModalOpen(false)} /> - setImportEntryModalOpen(false)} From e66040d32f399980f3e83141f06fc213072ddffe Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Thu, 3 Jun 2021 15:24:55 +0200 Subject: [PATCH 16/20] [Address Book v2] Allow empty owner names when loading a safe (#2390) * Allow to load new safe with empty owner names * Avoid adding owners to addressbook if name is empty * Remove unnecessary initialization --- src/components/forms/validator.ts | 8 +- .../load/components/DetailsForm/index.tsx | 14 +- src/routes/load/components/Layout.tsx | 9 +- .../load/components/OwnerList/index.tsx | 37 ++-- .../components/ReviewInformation/index.tsx | 182 +++++++++--------- src/routes/load/container/Load.tsx | 30 ++- 6 files changed, 138 insertions(+), 142 deletions(-) diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 5040e352..6b41b760 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -100,8 +100,12 @@ export const mustBeEthereumContractAddress = memoize( }, ) -export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => - value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols` +export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => { + const testValue = value || '' + return testValue.length >= +minLen && testValue.length <= +maxLen + ? undefined + : `Should be ${minLen} to ${maxLen} symbols` +} export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => { const decimals = value.split('.')[1] || '0' diff --git a/src/routes/load/components/DetailsForm/index.tsx b/src/routes/load/components/DetailsForm/index.tsx index 4adb2dbc..7385e607 100644 --- a/src/routes/load/components/DetailsForm/index.tsx +++ b/src/routes/load/components/DetailsForm/index.tsx @@ -1,7 +1,7 @@ import InputAdornment from '@material-ui/core/InputAdornment' import { makeStyles } from '@material-ui/core/styles' import CheckCircle from '@material-ui/icons/CheckCircle' -import * as React from 'react' +import React, { ReactElement, ReactNode } from 'react' import { FormApi } from 'final-form' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' @@ -68,7 +68,7 @@ interface DetailsFormProps { form: FormApi } -const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => { +const DetailsForm = ({ errors, form }: DetailsFormProps): ReactElement => { const classes = useStyles() const handleScan = (value: string, closeQrModal: () => void): void => { @@ -145,13 +145,11 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => } const DetailsPage = () => - function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement { + function LoadSafeDetails(controls: ReactNode, { errors, form }: StepperPageFormProps): ReactElement { return ( - <> - - - - + + + ) } diff --git a/src/routes/load/components/Layout.tsx b/src/routes/load/components/Layout.tsx index c1a889d0..893d7089 100644 --- a/src/routes/load/components/Layout.tsx +++ b/src/routes/load/components/Layout.tsx @@ -1,6 +1,6 @@ import IconButton from '@material-ui/core/IconButton' import ChevronLeft from '@material-ui/icons/ChevronLeft' -import * as React from 'react' +import React, { ReactElement } from 'react' import Stepper, { StepperPage } from 'src/components/Stepper' import Block from 'src/components/layout/Block' @@ -34,13 +34,12 @@ const formMutators = { } interface LayoutProps { - network: string provider?: string userAddress: string onLoadSafeSubmit: (values: LoadFormValues) => void } -const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProps): React.ReactElement => ( +const Layout = ({ onLoadSafeSubmit, provider, userAddress }: LayoutProps): ReactElement => ( <> {provider ? ( @@ -58,8 +57,8 @@ const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProp testId="load-safe-form" > - - + + ) : ( diff --git a/src/routes/load/components/OwnerList/index.tsx b/src/routes/load/components/OwnerList/index.tsx index 2f8ca790..afea0b75 100644 --- a/src/routes/load/components/OwnerList/index.tsx +++ b/src/routes/load/components/OwnerList/index.tsx @@ -1,12 +1,13 @@ +import { EthHashInfo } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import TableContainer from '@material-ui/core/TableContainer' -import React, { useEffect, useState } from 'react' - +import React, { ReactElement, ReactNode, useEffect, useState } from 'react' import { useSelector } from 'react-redux' +import { getExplorerInfo } from 'src/config' import Field from 'src/components/forms/Field' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' +import { minMaxLength } from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' @@ -21,8 +22,7 @@ import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields' import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields' import { styles } from './styles' -import { getExplorerInfo } from 'src/config' -import { EthHashInfo } from '@gnosis.pm/safe-react-components' +import { LoadFormValues } from 'src/routes/load/container/Load' const calculateSafeValues = (owners, threshold, values) => { const initialValues = { ...values } @@ -41,10 +41,14 @@ const useAddressBookForOwnersNames = (ownersList: string[]): AddressBookEntry[] const useStyles = makeStyles(styles) -const OwnerListComponent = (props) => { +interface OwnerListComponentProps { + values: LoadFormValues + updateInitialProps: (initialValues) => void +} + +const OwnerListComponent = ({ values, updateInitialProps }: OwnerListComponentProps): ReactElement => { const [owners, setOwners] = useState([]) const classes = useStyles() - const { updateInitialProps, values } = props const ownersWithNames = useAddressBookForOwnersNames(owners) @@ -88,19 +92,18 @@ const OwnerListComponent = (props) => { {ownersWithNames.map(({ address, name }, index) => { - const ownerName = name || `Owner #${index + 1}` return ( @@ -118,14 +121,12 @@ const OwnerListComponent = (props) => { ) } -const OwnerList = ({ updateInitialProps }, network) => - function LoadSafeOwnerList(controls, { values }): React.ReactElement { +const OwnerList = ({ updateInitialProps }) => + function LoadSafeOwnerList(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement { return ( - <> - - - - + + + ) } diff --git a/src/routes/load/components/ReviewInformation/index.tsx b/src/routes/load/components/ReviewInformation/index.tsx index 9156f1d0..d0820dbb 100644 --- a/src/routes/load/components/ReviewInformation/index.tsx +++ b/src/routes/load/components/ReviewInformation/index.tsx @@ -1,6 +1,8 @@ +import { EthHashInfo } from '@gnosis.pm/safe-react-components' import TableContainer from '@material-ui/core/TableContainer' -import React from 'react' +import React, { ReactElement, ReactNode } from 'react' +import { getExplorerInfo } from 'src/config' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' @@ -11,8 +13,6 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/ import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields' import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor' import { useStyles } from './styles' -import { getExplorerInfo } from 'src/config' -import { EthHashInfo } from '@gnosis.pm/safe-react-components' import { LoadFormValues } from 'src/routes/load/container/Load' const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => { @@ -33,108 +33,104 @@ interface Props { values: LoadFormValues } -const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => { +const ReviewComponent = ({ userAddress, values }: Props): ReactElement => { const classes = useStyles() const isOwner = checkIfUserAddressIsAnOwner(values, userAddress) const owners = getAccountsFrom(values) const safeAddress = values[FIELD_LOAD_ADDRESS] return ( - <> - - - - - - Review details - - - - - Name of the Safe - - - {values[FIELD_LOAD_NAME]} - - - - - Safe address - - - - - - - - Connected wallet client is owner? - - - {isOwner ? 'Yes' : 'No (read-only)'} - - - - - Any transaction requires the confirmation of: - - - {`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`} - - + + + + + + Review details + - - - - - - {`${getNumOwnersFrom(values)} Safe owners`} - - - - {owners.map((address, index) => ( - <> - - - - - - {index !== owners.length - 1 && } - - ))} - - - - + + + Name of the Safe + + + {values[FIELD_LOAD_NAME]} + + + + + Safe address + + + + + + + + Connected wallet client is owner? + + + {isOwner ? 'Yes' : 'No (read-only)'} + + + + + Any transaction requires the confirmation of: + + + {`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`} + + + + + + + + + {`${getNumOwnersFrom(values)} Safe owners`} + + + + {owners.map((address, index) => ( + <> + + + + + + {index !== owners.length - 1 && } + + ))} + + + ) } const Review = ({ userAddress }: { userAddress: string }) => - function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement { + function ReviewPage(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement { return ( - <> - - - - + + + ) } diff --git a/src/routes/load/container/Load.tsx b/src/routes/load/container/Load.tsx index 1d415754..40ff3d6f 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -1,9 +1,8 @@ import React, { ReactElement } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import Layout from 'src/routes/load/components/Layout' -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions' import { FIELD_LOAD_ADDRESS } from 'src/routes/load/components/fields' @@ -16,7 +15,7 @@ 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 { providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' export const loadSafe = async (safeAddress: string, addSafe: (safe: SafeRecordProps) => void): Promise => { @@ -51,7 +50,6 @@ export type LoadFormValues = ReviewSafeCreationValues | LoadForm const Load = (): ReactElement => { const dispatch = useDispatch() const provider = useSelector(providerNameSelector) - const network = useSelector(networkSelector) const userAddress = useSelector(userAccountSelector) const addSafeHandler = async (safe: SafeRecordProps) => { @@ -68,12 +66,17 @@ const Load = (): ReactElement => { const ownersNames = getNamesFrom(values) const ownersAddresses = getAccountsFrom(values) - const owners = ownersAddresses.map((address, index) => - makeAddressBookEntry({ - address, - name: ownersNames[index], - }), - ) + const owners = ownersAddresses.reduce((acc, address, index) => { + if (ownersNames[index]) { + // Do not add owners to addressbook if names are empty + const newAddressBookEntry = makeAddressBookEntry({ + address, + name: ownersNames[index], + }) + acc.push(newAddressBookEntry) + } + return acc + }, [] as AddressBookEntry[]) const safe = makeAddressBookEntry({ address: safeAddress, name: values.name }) await dispatch(addressBookSafeLoad([...owners, safe])) @@ -90,12 +93,7 @@ const Load = (): ReactElement => { return ( - + ) } From 50a7a49d1bcf97aacd8cdaf7714e7d0be250a31e Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 3 Jun 2021 11:49:09 -0300 Subject: [PATCH 17/20] keep safes with no name for the list of safes (#2387) * keep safes with no name for the list of safes * prevent storing only those safes whose `loadedViaUrl` flag is true * update AB before Safe store * default safe name to empty string if it is not available --- src/logic/safe/store/middleware/safeStorage.ts | 9 ++------- src/logic/safe/store/selectors/index.ts | 3 +-- src/routes/open/container/Open.tsx | 6 +++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/logic/safe/store/middleware/safeStorage.ts b/src/logic/safe/store/middleware/safeStorage.ts index 9184e2e8..091940f5 100644 --- a/src/logic/safe/store/middleware/safeStorage.ts +++ b/src/logic/safe/store/middleware/safeStorage.ts @@ -4,7 +4,7 @@ import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' -import { safesListWithAddressBookNameSelector, safesMapSelector } from 'src/logic/safe/store/selectors' +import { safesMapSelector } from 'src/logic/safe/store/selectors' import { SafeRecord } from '../models/safe' const watchedActions = [REMOVE_SAFE, SET_DEFAULT_SAFE, UPDATE_SAFE] @@ -24,12 +24,7 @@ export const safeStorageMiddleware = (store: Store) => ( if (watchedActions.includes(action.type)) { const state = store.getState() const safes = safesMapSelector(state) - const safeNameMap = Object.fromEntries( - safesListWithAddressBookNameSelector(state) - .map((safe) => [safe.address, safe.name]) - .toJSON(), - ) - await saveSafes(safes.filter((safe) => safeNameMap[safe.address]).toJSON()) + await saveSafes(safes.filter((safe) => !safe.loadedViaUrl).toJSON()) switch (action.type) { case SET_DEFAULT_SAFE: { diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 11c24303..145a5bcf 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -35,10 +35,9 @@ export const safesListWithAddressBookNameSelector = createSelector( .filter((safeRecord) => !safeRecord.loadedViaUrl) .map((safeRecord) => { const safe = safeRecord.toObject() - const name = addressBook?.[safe.address]?.name + const name = addressBook?.[safe.address]?.name ?? '' return { ...safe, name } }) - .filter((safeRecord: SafeRecordWithName) => safeRecord.name) }, ) diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index 771cf1f0..d27e5aca 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -177,13 +177,13 @@ const Open = (): ReactElement => { ownersAddresses = getAccountsFrom(pendingCreation) } - const safeProps = await buildSafe(safeAddress) - await dispatch(addOrUpdateSafe(safeProps)) - const owners = ownersAddresses.map((address, index) => makeAddressBookEntry({ address, name: ownersNames[index] })) const safe = makeAddressBookEntry({ address: safeAddress, name }) await dispatch(addressBookSafeLoad([...owners, safe])) + const safeProps = await buildSafe(safeAddress) + await dispatch(addOrUpdateSafe(safeProps)) + trackEvent({ category: 'User', action: 'Created a safe', From 2d532f303ac10d8e88d6a7d0d706481d4e6b2a13 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Thu, 3 Jun 2021 16:59:28 +0200 Subject: [PATCH 18/20] Set default order by name in address book (#2391) --- src/routes/safe/components/AddressBook/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 9672e7e2..f925e291 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -24,6 +24,7 @@ import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/Cre import { ExportEntriesModal } from 'src/routes/safe/components/AddressBook/ExportEntriesModal' import { DeleteEntryModal } from 'src/routes/safe/components/AddressBook/DeleteEntryModal' import { + AB_NAME_ID, AB_ADDRESS_ID, ADDRESS_BOOK_ROW_ID, SEND_ENTRY_BUTTON, @@ -199,6 +200,7 @@ const AddressBookTable = (): ReactElement => { columns={columns} data={addressBook} defaultFixed + defaultOrderBy={AB_NAME_ID} defaultRowsPerPage={25} disableLoadingOnEmptyTable label="Owners" From 5f3598aed576a6e049fd2be4c93bd033cacff267 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 7 Jun 2021 08:46:21 +0200 Subject: [PATCH 19/20] Bump version to 3.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c63d37a5..27c243f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "3.6.7", + "version": "3.7.0", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": { From c424c67d07f317ef4e43827e4ca6eee6f3959609 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 7 Jun 2021 09:21:33 +0200 Subject: [PATCH 20/20] Revert "Fix: don't display Intercom in Safe Apps (#2365)" This reverts commit 07465a0b2957fe7c9759815dc5b61de521a6e9a6. --- src/components/CookiesBanner/index.tsx | 11 ++--------- src/logic/hooks/useSafeAppUrl.tsx | 22 ---------------------- src/routes/safe/components/Apps/index.tsx | 16 +++++++++++----- 3 files changed, 13 insertions(+), 36 deletions(-) delete mode 100644 src/logic/hooks/useSafeAppUrl.tsx diff --git a/src/components/CookiesBanner/index.tsx b/src/components/CookiesBanner/index.tsx index 8b64d66e..2bae1916 100644 --- a/src/components/CookiesBanner/index.tsx +++ b/src/components/CookiesBanner/index.tsx @@ -9,7 +9,6 @@ import { COOKIES_KEY } from 'src/logic/cookies/model/cookie' import { openCookieBanner } from 'src/logic/cookies/store/actions/openCookieBanner' import { cookieBannerOpen } from 'src/logic/cookies/store/selectors' import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils' -import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl' import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables' import { loadGoogleAnalytics, removeCookies } from 'src/utils/googleAnalytics' import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom' @@ -98,7 +97,7 @@ interface CookiesBannerFormProps { const CookiesBanner = (): ReactElement => { const classes = useStyles() const dispatch = useRef(useDispatch()) - const { url: appUrl } = useSafeAppUrl() + const [showAnalytics, setShowAnalytics] = useState(false) const [showIntercom, setShowIntercom] = useState(false) const [localNecessary, setLocalNecessary] = useState(true) @@ -107,12 +106,6 @@ const CookiesBanner = (): ReactElement => { const showBanner = useSelector(cookieBannerOpen) - useEffect(() => { - if (appUrl) { - setTimeout(closeIntercom, 50) - } - }, [appUrl]) - useEffect(() => { async function fetchCookiesFromStorage() { const cookiesState = await loadFromCookie(COOKIES_KEY) @@ -178,7 +171,7 @@ const CookiesBanner = (): ReactElement => { dispatch.current(openCookieBanner({ cookieBannerOpen: false })) } - if (showIntercom && !appUrl) { + if (showIntercom) { loadIntercom() } diff --git a/src/logic/hooks/useSafeAppUrl.tsx b/src/logic/hooks/useSafeAppUrl.tsx deleted file mode 100644 index fa6b93f4..00000000 --- a/src/logic/hooks/useSafeAppUrl.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useLocation } from 'react-router-dom' -import { useEffect, useState } from 'react' - -type AppUrlReturnType = { - url: string | null -} - -export const useSafeAppUrl = (): AppUrlReturnType => { - const [url, setUrl] = useState(null) - const { search } = useLocation() - - useEffect(() => { - if (search !== url) { - const query = new URLSearchParams(search) - setUrl(query.get('appUrl')) - } - }, [search, url]) - - return { - url, - } -} diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx index 25b28588..5fa825c0 100644 --- a/src/routes/safe/components/Apps/index.tsx +++ b/src/routes/safe/components/Apps/index.tsx @@ -1,14 +1,20 @@ import React from 'react' -import { useSafeAppUrl } from 'src/logic/hooks/useSafeAppUrl' + +import { useLocation } from 'react-router-dom' import AppFrame from './components/AppFrame' import AppsList from './components/AppsList' -const Apps = (): React.ReactElement => { - const { url } = useSafeAppUrl() +const useQuery = () => { + return new URLSearchParams(useLocation().search) +} - if (url) { - return +const Apps = (): React.ReactElement => { + const query = useQuery() + const appUrl = query.get('appUrl') + + if (appUrl) { + return } else { return }