From 59dc1f711c246408e26ca8cde160a017b5c04746 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Tue, 22 Sep 2020 09:31:07 -0300 Subject: [PATCH] (Bugfix) - #1246 Addressbook entries removed when reloading page (#1300) * Fix addressbook types Restructure addressbook store type * Add more safe types * Fix imports * Removes .toJS() usage * Fix condition for saving addressBook * Types & remove send button from addressbook if user not an owner * Add types for addressBook actions Remove unused saveAndUpdateAddressBook action * Refactor addressBook: make it global and removes immutableJS Add types Removes unused addAddressBook action * Remove todo * Fix edit and remove entries style when user is not owner * Adds and updates safe name in addressBook * Adds checkIfOwnerWasDeletedFromAddressBook Let the user remove owners users without adding them again each time the safe loads * Simplify loadAddressBookFromStorage * Fix compilation errors included in pr #1301 * Uses sameAddress function * Add migration function for old stored address books * Update tests * Replaces shouldAvoidUpdatesNotifications with addAddressBookEntryOptions on addAddressBookEntry * Update tests * Unify return on getOwnersWithNameFromAddressBook * Reword shouldAvoidUpdatesNotifications * Replaces adbk with addressBook * Fix condition * Fix typos * Fix typo Co-authored-by: Daniel Sanchez --- src/logic/addressBook/model/addressBook.ts | 20 +-- .../store/actions/addAddressBook.ts | 8 - .../store/actions/addAddressBookEntry.ts | 21 ++- .../actions/addOrUpdateAddressBookEntry.ts | 4 +- .../store/actions/loadAddressBook.ts | 3 +- .../actions/loadAddressBookFromStorage.ts | 20 +-- .../store/actions/removeAddressBookEntry.ts | 2 +- .../store/actions/saveAndUpdateAddressBook.ts | 15 -- .../store/actions/updateAddressBookEntry.ts | 3 +- .../store/middleware/addressBookMiddleware.ts | 10 +- .../addressBook/store/reducer/addressBook.ts | 133 +++++------------ .../store/reducer/types/addressBook.d.ts | 24 --- .../addressBook/store/selectors/index.ts | 24 +-- .../utils/__tests__/addressBookUtils.test.ts | 141 +++++++++++++++--- src/logic/addressBook/utils/index.ts | 69 +++++++-- src/logic/safe/store/actions/addSafe.ts | 5 +- .../store/actions/loadSafesFromStorage.ts | 2 +- .../safe/store/middleware/safeStorage.ts | 67 ++++++++- .../safe/store/reducer/allTransactions.ts | 7 +- .../CreateEditEntryModal/index.tsx | 8 +- .../safe/components/AddressBook/columns.ts | 13 +- .../safe/components/AddressBook/index.tsx | 108 +++++++------- .../safe/components/AddressBook/style.ts | 11 +- .../screens/AddressBookInput/index.tsx | 24 +-- .../screens/SendCollectible/index.tsx | 24 +-- .../screens/SendCollectible/style.ts | 3 +- .../SendModal/screens/SendFunds/index.tsx | 12 +- .../ManageOwners/EditOwnerModal/index.tsx | 4 +- .../OwnerAddressTableCell/index.tsx | 3 +- .../Settings/ManageOwners/index.tsx | 6 +- src/routes/safe/components/Settings/index.tsx | 4 +- src/store/index.ts | 12 +- 32 files changed, 460 insertions(+), 350 deletions(-) delete mode 100644 src/logic/addressBook/store/actions/addAddressBook.ts delete mode 100644 src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts delete mode 100644 src/logic/addressBook/store/reducer/types/addressBook.d.ts diff --git a/src/logic/addressBook/model/addressBook.ts b/src/logic/addressBook/model/addressBook.ts index 34554330..8575448f 100644 --- a/src/logic/addressBook/model/addressBook.ts +++ b/src/logic/addressBook/model/addressBook.ts @@ -1,15 +1,17 @@ -import { Record, RecordOf } from 'immutable' - -export interface AddressBookEntryProps { +export type AddressBookEntry = { address: string name: string - isOwner: boolean } -export const makeAddressBookEntry = Record({ - address: '', - name: '', - isOwner: false, +export const makeAddressBookEntry = ({ + address = '', + name = '', +}: { + address: string + name?: string +}): AddressBookEntry => ({ + address, + name, }) -export type AddressBookEntryRecord = RecordOf +export type AddressBookState = AddressBookEntry[] diff --git a/src/logic/addressBook/store/actions/addAddressBook.ts b/src/logic/addressBook/store/actions/addAddressBook.ts deleted file mode 100644 index 7f524bbd..00000000 --- a/src/logic/addressBook/store/actions/addAddressBook.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK' - -export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook, safeAddress) => ({ - addressBook, - safeAddress, -})) diff --git a/src/logic/addressBook/store/actions/addAddressBookEntry.ts b/src/logic/addressBook/store/actions/addAddressBookEntry.ts index 76041d67..b3d80b50 100644 --- a/src/logic/addressBook/store/actions/addAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/addAddressBookEntry.ts @@ -1,7 +1,22 @@ import { createAction } from 'redux-actions' +import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook' export const ADD_ENTRY = 'ADD_ENTRY' -export const addAddressBookEntry = createAction(ADD_ENTRY, (entry) => ({ - 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 index f88f9b50..f19a0078 100644 --- a/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry.ts @@ -1,8 +1,8 @@ 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, (entryAddress, entry) => ({ - entryAddress, +export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({ entry, })) diff --git a/src/logic/addressBook/store/actions/loadAddressBook.ts b/src/logic/addressBook/store/actions/loadAddressBook.ts index 6486d1cb..d887d198 100644 --- a/src/logic/addressBook/store/actions/loadAddressBook.ts +++ b/src/logic/addressBook/store/actions/loadAddressBook.ts @@ -1,7 +1,8 @@ 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) => ({ +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 index 0e40bae6..c4315e3c 100644 --- a/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts +++ b/src/logic/addressBook/store/actions/loadAddressBookFromStorage.ts @@ -1,29 +1,17 @@ -import { List } from 'immutable' - 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 { safesListSelector } from 'src/logic/safe/store/selectors' +import { Dispatch } from 'redux' -const loadAddressBookFromStorage = () => async (dispatch, getState) => { +const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise => { try { - const state = getState() let storedAdBk = await getAddressBookFromStorage() if (!storedAdBk) { storedAdBk = [] } - let addressBook = buildAddressBook(storedAdBk) - // Fetch all the current safes, in case that we don't have a safe on the adbk, we add it - const safes = safesListSelector(state) - const adbkEntries = addressBook.keySeq().toArray() - safes.forEach((safe) => { - const { address } = safe - const found = adbkEntries.includes(address) - if (!found) { - addressBook = addressBook.set(address, List([])) - } - }) + const addressBook = buildAddressBook(storedAdBk) + dispatch(loadAddressBook(addressBook)) } catch (err) { // eslint-disable-next-line diff --git a/src/logic/addressBook/store/actions/removeAddressBookEntry.ts b/src/logic/addressBook/store/actions/removeAddressBookEntry.ts index a241e1c1..16571f47 100644 --- a/src/logic/addressBook/store/actions/removeAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/removeAddressBookEntry.ts @@ -2,6 +2,6 @@ import { createAction } from 'redux-actions' export const REMOVE_ENTRY = 'REMOVE_ENTRY' -export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress) => ({ +export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({ entryAddress, })) diff --git a/src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts b/src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts deleted file mode 100644 index 960ee92b..00000000 --- a/src/logic/addressBook/store/actions/saveAndUpdateAddressBook.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' -import { saveAddressBook } from 'src/logic/addressBook/utils' - -const saveAndUpdateAddressBook = (addressBook) => async (dispatch) => { - try { - dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook))) - await saveAddressBook(addressBook) - } catch (err) { - // eslint-disable-next-line - console.error('Error while loading active tokens from storage:', err) - } -} - -export default saveAndUpdateAddressBook diff --git a/src/logic/addressBook/store/actions/updateAddressBookEntry.ts b/src/logic/addressBook/store/actions/updateAddressBookEntry.ts index 38f8263a..a426f812 100644 --- a/src/logic/addressBook/store/actions/updateAddressBookEntry.ts +++ b/src/logic/addressBook/store/actions/updateAddressBookEntry.ts @@ -1,7 +1,8 @@ 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) => ({ +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 index 9765be48..fb7075dd 100644 --- a/src/logic/addressBook/store/middleware/addressBookMiddleware.ts +++ b/src/logic/addressBook/store/middleware/addressBookMiddleware.ts @@ -2,7 +2,7 @@ import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEnt 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 { addressBookMapSelector } from 'src/logic/addressBook/store/selectors' +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' @@ -16,15 +16,15 @@ const addressBookMiddleware = (store) => (next) => async (action) => { if (watchedActions.includes(action.type)) { const state = store.getState() const { dispatch } = store - const addressBook = addressBookMapSelector(state) - if (addressBook) { + const addressBook = addressBookSelector(state) + if (addressBook.length) { await saveAddressBook(addressBook) } switch (action.type) { case ADD_ENTRY: { - const { isOwner } = action.payload.entry - if (!isOwner) { + const { shouldAvoidUpdatesNotifications } = action.payload + if (!shouldAvoidUpdatesNotifications) { const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY) dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded))) } diff --git a/src/logic/addressBook/store/reducer/addressBook.ts b/src/logic/addressBook/store/reducer/addressBook.ts index e1ce17d6..55f3a493 100644 --- a/src/logic/addressBook/store/reducer/addressBook.ts +++ b/src/logic/addressBook/store/reducer/addressBook.ts @@ -1,134 +1,71 @@ -import { List, Map } from 'immutable' import { handleActions } from 'redux-actions' -import { - AddressBookEntryRecord, - AddressBookEntryProps, - makeAddressBookEntry, -} from 'src/logic/addressBook/model/addressBook' -import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook' +import { 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 { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils' -import { sameAddress } from 'src/logic/wallets/ethAddresses' import { checksumAddress } from 'src/utils/checksumAddress' +import { getValidAddressBookName } from 'src/logic/addressBook/utils' export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' -export type AddressBookCollection = List -export type AddressBookState = Map> - -export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map => { - let addressBookBuilt: Map = Map([]) - Object.entries(storedAdbk).forEach((adbkProps: any) => { - const safeAddress = checksumAddress(adbkProps[0]) - const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry) - const adbkSafeEntries = List(adbkRecords) - addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries) +export const buildAddressBook = (storedAdbk: AddressBookState): AddressBookState => { + return storedAdbk.map((addressBookEntry) => { + const { address, name } = addressBookEntry + return makeAddressBookEntry({ address: checksumAddress(address), name }) }) - return addressBookBuilt } export default handleActions( { [LOAD_ADDRESS_BOOK]: (state, action) => { const { addressBook } = action.payload - return state.set('addressBook', addressBook) - }, - [ADD_ADDRESS_BOOK]: (state, action) => { - const { addressBook, safeAddress } = action.payload - // Adds the address book if it does not exists - const found = state.getIn(['addressBook', safeAddress]) - if (!found) { - return state.setIn(['addressBook', safeAddress], addressBook) - } - return state + return addressBook }, [ADD_ENTRY]: (state, action) => { const { entry } = action.payload - // Adds the entry to all the safes (if it does not already exists) - const newState = state.withMutations((map) => { - const adbkMap = map.get('addressBook') + const entryFound = state.find((oldEntry) => oldEntry.address === entry.address) - if (adbkMap) { - adbkMap.keySeq().forEach((safeAddress) => { - const safeAddressBook = state.getIn(['addressBook', safeAddress]) - - if (safeAddressBook) { - const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook) - const found = adbkAddressList.includes(entry.address) - if (!found) { - const updatedSafeAdbkList = safeAddressBook.push(entry) - map.setIn(['addressBook', safeAddress], updatedSafeAdbkList) - } - } - }) - } - }) - return newState + // Only adds entries with valid names + const validName = getValidAddressBookName(entry.name) + if (!entryFound && validName) { + state.push(entry) + } + return state }, [UPDATE_ENTRY]: (state, action) => { const { entry } = action.payload - - // Updates the entry from all the safes - const newState = state.withMutations((map) => { - map - .get('addressBook') - .keySeq() - .forEach((safeAddress) => { - const entriesList = state.getIn(['addressBook', safeAddress]) - const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address)) - const updatedEntriesList = entriesList.set(entryIndex, entry) - map.setIn(['addressBook', safeAddress], updatedEntriesList) - }) - }) - - return newState + const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address) + if (entryIndex >= 0) { + state[entryIndex] = entry + } + return state }, [REMOVE_ENTRY]: (state, action) => { const { entryAddress } = action.payload - // Removes the entry from all the safes - const newState = state.withMutations((map) => { - map - .get('addressBook') - .keySeq() - .forEach((safeAddress) => { - const entriesList = state.getIn(['addressBook', safeAddress]) - const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress)) - const updatedEntriesList = entriesList.remove(entryIndex) - map.setIn(['addressBook', safeAddress], updatedEntriesList) - }) - }) - return newState + const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress) + state.splice(entryIndex, 1) + return state }, [ADD_OR_UPDATE_ENTRY]: (state, action) => { const { entry, entryAddress } = action.payload - // Adds or Updates the entry to all the safes - return state.withMutations((map) => { - const addressBook = map.get('addressBook') - if (addressBook) { - addressBook.keySeq().forEach((safeAddress) => { - const safeAddressBook = state.getIn(['addressBook', safeAddress]) - const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress)) - - if (entryIndex !== -1) { - const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry)) - map.setIn(['addressBook', safeAddress], updatedEntriesList) - } else { - const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry)) - map.setIn(['addressBook', safeAddress], updatedSafeAdbkList) - } - }) - } - }) + // Only updates entries with valid names + const validName = getValidAddressBookName(entry.name) + if (!validName) { + return state + } + const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress) + if (entryIndex >= 0) { + state[entryIndex] = entry + } else { + state.push(entry) + } + return state }, }, - Map({ - addressBook: Map({}), - }), + [], ) diff --git a/src/logic/addressBook/store/reducer/types/addressBook.d.ts b/src/logic/addressBook/store/reducer/types/addressBook.d.ts deleted file mode 100644 index 6f2f4c7a..00000000 --- a/src/logic/addressBook/store/reducer/types/addressBook.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AddressBookEntryRecord, AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook' -import { Map, List } from 'immutable' - -export interface AddressBookReducerState { - addressBook: AddressBookMap -} - -interface AddressBookMapSerialized { - [key: string]: AddressBookEntryProps -} - -interface AddressBookReducerStateSerialized extends AddressBookReducerState { - addressBook: Record -} - -export interface AddressBookMap extends Map { - toJS(): AddressBookMapSerialized - get(key: string, notSetValue: unknown): List -} - -export interface AddressBookReducerMap extends Map { - toJS(): AddressBookReducerStateSerialized - get(key: K): AddressBookReducerState[K] -} diff --git a/src/logic/addressBook/store/selectors/index.ts b/src/logic/addressBook/store/selectors/index.ts index b03fbdf1..ffa4fbbe 100644 --- a/src/logic/addressBook/store/selectors/index.ts +++ b/src/logic/addressBook/store/selectors/index.ts @@ -1,36 +1,22 @@ import { AppReduxState } from 'src/store' -import { List } from 'immutable' + import { createSelector } from 'reselect' import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook' -import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d' -import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -export const addressBookMapSelector = (state: AppReduxState): AddressBookMap => - state[ADDRESS_BOOK_REDUCER_ID].get('addressBook') +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' -export const getAddressBook = createSelector( - addressBookMapSelector, - safeParamAddressFromStateSelector, - (addressBook, safeAddress) => { - let result: List = List([]) - if (addressBook && safeAddress) { - result = addressBook.get(safeAddress, List()) - } - return result - }, -) +export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID] export const getNameFromAddressBook = createSelector( - getAddressBook, + addressBookSelector, (_, address) => address, (addressBook, address) => { const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address) + if (adbkEntry) { return adbkEntry.name } - return 'UNKNOWN' }, ) diff --git a/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts index f9574c29..63599d72 100644 --- a/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts +++ b/src/logic/addressBook/utils/__tests__/addressBookUtils.test.ts @@ -1,25 +1,31 @@ -import { Map, List } from 'immutable' +import { List } from 'immutable' import { getAddressBookFromStorage, - getAddressesListFromSafeAddressBook, - getNameFromSafeAddressBook, + getAddressesListFromAddressBook, + getNameFromAddressBook, getOwnersWithNameFromAddressBook, + migrateOldAddressBook, + OldAddressBookEntry, + OldAddressBookType, saveAddressBook, } from 'src/logic/addressBook/utils/index' import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook' -import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -const getMockAddressBookEntry = ( - address: string, - name: string = 'test', - isOwner: boolean = false, -): AddressBookEntryRecord => +const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry => makeAddressBookEntry({ address, name, - isOwner, }) +const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => { + return { + address, + name, + isOwner, + } +} + describe('getAddressesListFromAdbk', () => { const entry1 = getMockAddressBookEntry('123456', 'test1') const entry2 = getMockAddressBookEntry('78910', 'test2') @@ -27,11 +33,11 @@ describe('getAddressesListFromAdbk', () => { it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => { // given - const safeAddressBook = List([entry1, entry2, entry3]) + const safeAddressBook = [entry1, entry2, entry3] const expectedResult = [entry1.address, entry2.address, entry3.address] // when - const result = getAddressesListFromSafeAddressBook(safeAddressBook) + const result = getAddressesListFromAddressBook(safeAddressBook) // then expect(result).toStrictEqual(expectedResult) @@ -44,11 +50,11 @@ describe('getNameFromSafeAddressBook', () => { const entry3 = getMockAddressBookEntry('4781321', 'test3') it('It should returns the user name given a safeAddressBook and an user account', () => { // given - const safeAddressBook = List([entry1, entry2, entry3]) + const safeAddressBook = [entry1, entry2, entry3] const expectedResult = entry2.name // when - const result = getNameFromSafeAddressBook(safeAddressBook, entry2.address) + const result = getNameFromAddressBook(safeAddressBook, entry2.address) // then expect(result).toBe(expectedResult) @@ -61,7 +67,7 @@ describe('getOwnersWithNameFromAddressBook', () => { 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 = List([entry1, entry2, entry3]) + const safeAddressBook = [entry1, entry2, entry3] const ownerList = List([ { address: entry1.address, name: '' }, { address: entry2.address, name: '' }, @@ -80,14 +86,15 @@ describe('getOwnersWithNameFromAddressBook', () => { }) describe('saveAddressBook', () => { - const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' - const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503' - const entry1 = getMockAddressBookEntry('123456', 'test1') - const entry2 = getMockAddressBookEntry('78910', 'test2') - const entry3 = getMockAddressBookEntry('4781321', 'test3') + const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' + const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' + const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126' + const entry1 = getMockAddressBookEntry(mockAdd1, 'test1') + const entry2 = getMockAddressBookEntry(mockAdd2, 'test2') + const entry3 = getMockAddressBookEntry(mockAdd3, 'test3') it('It should save a given addressBook to the localStorage', async () => { // given - const addressBook = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) }) + const addressBook: AddressBookState = [entry1, entry2, entry3] // when // @ts-ignore @@ -100,3 +107,95 @@ describe('saveAddressBook', () => { expect(result).toStrictEqual(addressBook) }) }) + +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) + }) +}) + +jest.mock('src/utils/storage/index') +describe('getAddressBookFromStorage', () => { + const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A' + const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00' + const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91' + const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8' + 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() + }) +}) diff --git a/src/logic/addressBook/utils/index.ts b/src/logic/addressBook/utils/index.ts index 8b636892..73743000 100644 --- a/src/logic/addressBook/utils/index.ts +++ b/src/logic/addressBook/utils/index.ts @@ -1,28 +1,62 @@ import { List } from 'immutable' import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook' +import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { SafeOwner } from 'src/logic/safe/store/models/safe' -import { AddressBookCollection } from '../store/reducer/addressBook' -import { AddressBookMap } from '../store/reducer/types/addressBook' +import { sameAddress } from 'src/logic/wallets/ethAddresses' const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' -export const getAddressBookFromStorage = async (): Promise | undefined> => { - return await loadFromStorage>(ADDRESS_BOOK_STORAGE_KEY) +export type OldAddressBookEntry = { + address: string + name: string + isOwner: boolean } -export const saveAddressBook = async (addressBook: AddressBookMap): Promise => { +export type OldAddressBookType = { + [safeAddress: string]: [OldAddressBookEntry] +} + +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, addressBook.toJS()) + await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook)) } catch (err) { console.error('Error storing addressBook in localstorage', err) } } -export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] => - Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address) +export const getAddressesListFromAddressBook = (addressBook: AddressBookState): string[] => + addressBook.map((entry) => entry.address) -export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => { +export const getNameFromAddressBook = (addressBook: AddressBookState, userAddress: string): string | null => { const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress) if (entry) { return entry.name @@ -30,15 +64,22 @@ export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, u return null } +export const getValidAddressBookName = (addressbookName: string): string | null => { + const INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET'] + const isInvalid = INVALID_NAMES.find((invalidName) => addressbookName.toUpperCase().includes(invalidName)) + if (isInvalid) return null + return addressbookName +} + export const getOwnersWithNameFromAddressBook = ( - addressBook: AddressBookCollection, + addressBook: AddressBookState, ownerList: List, -): List | [] => { +): List => { if (!ownerList) { - return [] + return List([]) } return ownerList.map((owner) => { - const ownerName = getNameFromSafeAddressBook(addressBook, owner.address) + const ownerName = getNameFromAddressBook(addressBook, owner.address) return { address: owner.address, name: ownerName || owner.name, diff --git a/src/logic/safe/store/actions/addSafe.ts b/src/logic/safe/store/actions/addSafe.ts index 0317a22f..90851dab 100644 --- a/src/logic/safe/store/actions/addSafe.ts +++ b/src/logic/safe/store/actions/addSafe.ts @@ -18,15 +18,16 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List ({ +export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({ safe, + loadedFromStorage, })) const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => { const state = getState() const safeList = safesListSelector(state) - dispatch(addSafe(safe)) + dispatch(addSafe(safe, true)) if (safeList.size === 0) { dispatch(setDefaultSafe(safe.address)) diff --git a/src/logic/safe/store/actions/loadSafesFromStorage.ts b/src/logic/safe/store/actions/loadSafesFromStorage.ts index de752b0e..c6009857 100644 --- a/src/logic/safe/store/actions/loadSafesFromStorage.ts +++ b/src/logic/safe/store/actions/loadSafesFromStorage.ts @@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise => if (safes) { Object.values(safes).forEach((safeProps) => { - dispatch(addSafe(buildSafe(safeProps))) + dispatch(addSafe(buildSafe(safeProps), true)) }) } } catch (err) { diff --git a/src/logic/safe/store/middleware/safeStorage.ts b/src/logic/safe/store/middleware/safeStorage.ts index e5b2feef..4f588d0b 100644 --- a/src/logic/safe/store/middleware/safeStorage.ts +++ b/src/logic/safe/store/middleware/safeStorage.ts @@ -1,4 +1,3 @@ -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry' import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils' import { tokensSelector } from 'src/logic/tokens/store/selectors' @@ -13,6 +12,12 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors' +import { checksumAddress } from 'src/utils/checksumAddress' +import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' +import { getValidAddressBookName } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { sameAddress } from 'src/logic/wallets/ethAddresses' const watchedActions = [ ADD_SAFE, @@ -41,6 +46,30 @@ const recalculateActiveTokens = (state) => { saveActiveTokens(activeTokens) } +/** + * If the owner has a valid name that means that should be on the addressBook + * if the owner is not currently on the addressBook, that means the user deleted it + * or that it's a new safe with valid names, so we also check if it's a new safe or an already loaded one + * @param name + * @param address + * @param addressBook + * @param safeAlreadyLoaded -> true if the safe was loaded from the localStorage + */ +// TODO TEST +const checkIfOwnerWasDeletedFromAddressBook = ( + { name, address }: AddressBookEntry, + addressBook: AddressBookState, + safeAlreadyLoaded: boolean, +) => { + if (!safeAlreadyLoaded) { + return false + } + + const addressShouldBeOnTheAddressBook = !!getValidAddressBookName(name) + const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address)) + return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook +} + const safeStorageMware = (store) => (next) => async (action) => { const handledAction = next(action) @@ -48,6 +77,7 @@ const safeStorageMware = (store) => (next) => async (action) => { const state = store.getState() const { dispatch } = store const safes = safesMapSelector(state) + const addressBook = addressBookSelector(state) await saveSafes(safes.toJSON()) switch (action.type) { @@ -56,19 +86,42 @@ const safeStorageMware = (store) => (next) => async (action) => { break } case ADD_SAFE: { - const { safe } = action.payload - const ownersArray = safe.owners.toJS() - // Adds the owners to the address book - ownersArray.forEach((owner) => { - dispatch(addAddressBookEntry(makeAddressBookEntry({ ...owner, isOwner: true }))) + const { safe, loadedFromStorage } = action.payload + safe.owners.forEach((owner) => { + const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name }) + const ownerWasAlreadyInAddressBook = checkIfOwnerWasDeletedFromAddressBook( + checksumEntry, + addressBook, + loadedFromStorage, + ) + + if (!ownerWasAlreadyInAddressBook) { + dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false })) + } }) + const safeWasAlreadyInAddressBook = checkIfOwnerWasDeletedFromAddressBook( + { address: safe.address, name: safe.name }, + addressBook, + loadedFromStorage, + ) + + if (!safeWasAlreadyInAddressBook) { + dispatch( + addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), { + notifyEntryUpdate: true, + }), + ) + } break } case UPDATE_SAFE: { - const { activeTokens } = action.payload + const { activeTokens, name, address } = action.payload if (activeTokens) { recalculateActiveTokens(state) } + if (name) { + dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })), { notifyEntryUpdate: false }) + } break } case SET_DEFAULT_SAFE: { diff --git a/src/logic/safe/store/reducer/allTransactions.ts b/src/logic/safe/store/reducer/allTransactions.ts index 4e95bb7d..48445649 100644 --- a/src/logic/safe/store/reducer/allTransactions.ts +++ b/src/logic/safe/store/reducer/allTransactions.ts @@ -1,7 +1,10 @@ import { handleActions } from 'redux-actions' -import { Transaction } from '../models/types/transactions' -import { LOAD_MORE_TRANSACTIONS, LoadMoreTransactionsAction } from '../actions/allTransactions/pagination' +import { Transaction } from 'src/logic/safe/store/models/types/transactions' +import { + LOAD_MORE_TRANSACTIONS, + LoadMoreTransactionsAction, +} from 'src/logic/safe/store/actions/allTransactions/pagination' export const TRANSACTIONS = 'allTransactions' diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx index 628a7af0..c8c5073a 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.tsx @@ -19,8 +19,8 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getAddressesListFromAddressBook } from 'src/logic/addressBook/utils' export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name' export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address' @@ -42,8 +42,8 @@ const CreateEditEntryModalComponent = ({ } } - const addressBook = useSelector(getAddressBook) - const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook) + const addressBook = useSelector(addressBookSelector) + const addressBookAddressesList = getAddressesListFromAddressBook(addressBook) const entryDoesntExist = uniqueAddress(addressBookAddressesList) const formMutators = { diff --git a/src/routes/safe/components/AddressBook/columns.ts b/src/routes/safe/components/AddressBook/columns.ts index 6548e18a..79ddf531 100644 --- a/src/routes/safe/components/AddressBook/columns.ts +++ b/src/routes/safe/components/AddressBook/columns.ts @@ -1,4 +1,5 @@ import { List } from 'immutable' +import { TableCellProps } from '@material-ui/core/TableCell/TableCell' export const ADDRESS_BOOK_ROW_ID = 'address-book-row' export const TX_TABLE_ADDRESS_BOOK_ID = 'idAddressBook' @@ -9,7 +10,17 @@ export const EDIT_ENTRY_BUTTON = 'edit-entry-btn' export const REMOVE_ENTRY_BUTTON = 'remove-entry-btn' export const SEND_ENTRY_BUTTON = 'send-entry-btn' -export const generateColumns = () => { +type AddressBookColumn = { + id: string + order: boolean + disablePadding?: boolean + label: string + width?: number + custom?: boolean + align?: TableCellProps['align'] +} + +export const generateColumns = (): List => { const nameColumn = { id: AB_NAME_ID, order: false, diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index 9362b09e..42597e64 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -1,10 +1,8 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' -import { withStyles } from '@material-ui/core/styles' -// import CallMade from '@material-ui/icons/CallMade' +import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' -// import classNames from 'classnames/bind' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -18,11 +16,11 @@ 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' -import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +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 { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses' import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal' import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal' @@ -38,19 +36,32 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' 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 RemoveOwnerIconDisabled from 'src/routes/safe/components/Settings/assets/icons/disabled-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 { getValidAddressBookName } from 'src/logic/addressBook/utils' -const AddressBookTable = ({ classes }) => { +const useStyles = makeStyles(styles) + +interface AddressBookSelectedEntry extends AddressBookEntry { + isNew?: boolean +} + +const AddressBookTable = (): React.ReactElement => { + const classes = useStyles() const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const dispatch = useDispatch() const safesList = useSelector(safesListSelector) const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector) - const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState(null) + const addressBook = useSelector(addressBookSelector) + const granted = useSelector(grantedSelector) + const [selectedEntry, setSelectedEntry] = useState<{ + entry?: AddressBookSelectedEntry + index?: number + isOwnerAddress?: boolean + } | null>(null) const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false) const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false) const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false) @@ -69,11 +80,10 @@ const AddressBookTable = ({ classes }) => { useEffect(() => { if (entryAddressToEditOrCreateNew) { const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew) - const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd) - if (key && key >= 0) { + const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd) + if (oldEntryIndex >= 0) { // Edit old entry - const value = addressBook.get(key) - setSelectedEntry({ entry: value, index: key }) + setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex }) } else { // Create new entry setSelectedEntry({ @@ -107,7 +117,7 @@ const AddressBookTable = ({ classes }) => { } const deleteEntryModalHandler = () => { - const entryAddress = checksumAddress(selectedEntry.entry.address) + const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : '' setSelectedEntry(null) setDeleteEntryModalOpen(false) dispatch(removeAddressBookEntry(entryAddress)) @@ -138,7 +148,7 @@ const AddressBookTable = ({ classes }) => { defaultRowsPerPage={25} disableLoadingOnEmptyTable label="Owners" - size={addressBook?.size || 0} + size={addressBook?.length || 0} > {(sortedData) => sortedData.map((row, index) => { @@ -151,20 +161,22 @@ const AddressBookTable = ({ classes }) => { key={index} tabIndex={-1} > - {autoColumns.map((column: any) => ( - - {column.id === AB_ADDRESS_ID ? ( - - ) : ( - row[column.id] - )} - - ))} + {autoColumns.map((column) => { + return ( + + {column.id === AB_ADDRESS_ID ? ( + + ) : ( + getValidAddressBookName(row[column.id]) + )} + + ) + })} Edit entry { setSelectedEntry({ entry: row, @@ -177,33 +189,29 @@ const AddressBookTable = ({ classes }) => { /> Remove entry { - if (!userOwner) { - setSelectedEntry({ entry: row }) - setDeleteEntryModalOpen(true) - } - }} - src={userOwner ? RemoveOwnerIconDisabled : RemoveOwnerIcon} - testId={REMOVE_ENTRY_BUTTON} - /> - + src={RemoveOwnerIcon} + testId={REMOVE_ENTRY_BUTTON} + /> + {granted ? ( + + ) : null} @@ -236,4 +244,4 @@ const AddressBookTable = ({ classes }) => { ) } -export default withStyles(styles as any)(AddressBookTable) +export default AddressBookTable diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index b676dd63..f0e6c980 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -1,6 +1,7 @@ import { lg, marginButtonImg, md, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ formContainer: { minHeight: '250px', }, @@ -38,6 +39,9 @@ export const styles = () => ({ cursor: 'pointer', marginBottom: marginButtonImg, }, + editEntryButtonNonOwner: { + cursor: 'pointer', + }, removeEntryButton: { marginLeft: lg, marginRight: lg, @@ -50,6 +54,11 @@ export const styles = () => ({ marginBottom: marginButtonImg, cursor: 'default', }, + removeEntryButtonNonOwner: { + marginLeft: lg, + marginRight: lg, + cursor: 'pointer', + }, message: { padding: `${md} 0`, maxHeight: '54px', 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 ddd0791d..f766ea53 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/AddressBookInput/index.tsx @@ -1,7 +1,6 @@ import MuiTextField from '@material-ui/core/TextField' import makeStyles from '@material-ui/core/styles/makeStyles' import Autocomplete from '@material-ui/lab/Autocomplete' -import { List } from 'immutable' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { trimSpaces } from 'src/utils/strings' @@ -10,10 +9,10 @@ import { styles } from './style' import Identicon from 'src/components/Identicon' import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator' -import { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { getAddressFromENS } from 'src/logic/wallets/getWeb3' import { isValidEnsName } from 'src/logic/wallets/ethAddresses' -import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook' +import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook' export interface AddressBookProps { fieldMutator: (address: string) => void @@ -44,12 +43,10 @@ const textFieldInputStyle = makeStyles(() => ({ }, })) -const filterAddressBookWithContractAddresses = async ( - addressBook: List, -): Promise> => { +const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise => { const abFlags = await Promise.all( addressBook.map( - async ({ address }: AddressBookEntryRecord): Promise => { + async ({ address }: AddressBookEntry): Promise => { return (await mustBeEthereumContractAddress(address)) === undefined }, ), @@ -58,11 +55,6 @@ const filterAddressBookWithContractAddresses = async ( return addressBook.filter((_, index) => abFlags[index]) } -interface FilteredAddressBookEntry { - name: string - address: string -} - const AddressBookInput = ({ fieldMutator, isCustomTx, @@ -72,12 +64,12 @@ const AddressBookInput = ({ setSelectedEntry, }: AddressBookProps): React.ReactElement => { const classes = useStyles() - const addressBook = useSelector(getAddressBook) + const addressBook = useSelector(addressBookSelector) const [isValidForm, setIsValidForm] = useState(true) const [validationText, setValidationText] = useState('') const [inputTouched, setInputTouched] = useState(false) const [blurred, setBlurred] = useState(pristine) - const [adbkList, setADBKList] = useState>(List([])) + const [adbkList, setADBKList] = useState([]) const [inputAddValue, setInputAddValue] = useState(recipientAddress) @@ -168,7 +160,7 @@ const AddressBookInput = ({ freeSolo getOptionLabel={(adbkEntry) => adbkEntry.address || ''} id="free-solo-demo" - onChange={(_, value: FilteredAddressBookEntry) => { + onChange={(_, value: AddressBookEntry) => { let address = '' let name = '' if (value) { @@ -184,7 +176,7 @@ const AddressBookInput = ({ setBlurred(false) }} open={!blurred} - options={adbkList.toArray()} + options={adbkList} renderInput={(params) => ( { +const SendCollectible = ({ + initialValues, + onClose, + onNext, + recipientAddress, + selectedToken = {}, +}): React.ReactElement => { const classes = useStyles() const nftAssets = useSelector(safeActiveSelectorMap) const nftTokens = useSelector(nftTokensSelector) - const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState({ + const addressBook = useSelector(addressBookSelector) + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: recipientAddress || initialValues.recipientAddress, name: '', }) @@ -64,7 +70,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel const handleSubmit = (values) => { // If the input wasn't modified, there was no mutation of the recipientAddress if (!values.recipientAddress) { - values.recipientAddress = selectedEntry.address + values.recipientAddress = selectedEntry?.address } values.assetName = nftAssets[values.assetAddress].name @@ -97,10 +103,10 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ - name: scannedName || '', + name: scannedName, address: scannedAddress, }) closeQrModal() diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts index 44d7d9bb..776dea04 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/style.ts @@ -1,6 +1,7 @@ import { lg, md, secondaryText } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 11b293d8..27db52e9 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -25,8 +25,8 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors' -import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' @@ -69,8 +69,8 @@ const SendFunds = ({ }: SendFundsProps): React.ReactElement => { const classes = useStyles() const tokens = useSelector(extendedSafeTokensSelector) - const addressBook = useSelector(getAddressBook) - const [selectedEntry, setSelectedEntry] = useState({ + const addressBook = useSelector(addressBookSelector) + const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({ address: recipientAddress || initialValues.recipientAddress, name: '', }) @@ -88,7 +88,7 @@ const SendFunds = ({ const submitValues = values // If the input wasn't modified, there was no mutation of the recipientAddress if (!values.recipientAddress) { - submitValues.recipientAddress = selectedEntry.address + submitValues.recipientAddress = selectedEntry?.address } onNext(submitValues) } @@ -118,7 +118,7 @@ const SendFunds = ({ if (scannedAddress.startsWith('ethereum:')) { scannedAddress = scannedAddress.replace('ethereum:', '') } - const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : '' + const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : '' mutators.setRecipient(scannedAddress) setSelectedEntry({ name: scannedName || '', diff --git a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx index caac7551..7f6fbec5 100644 --- a/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/EditOwnerModal/index.tsx @@ -20,12 +20,12 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' -import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry' import { NOTIFICATIONS } from 'src/logic/notifications' import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { sm } from 'src/theme/variables' import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' +import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input' export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn' @@ -47,7 +47,7 @@ const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName } const { ownerName } = values dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName })) - dispatch(updateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName }))) + dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName }))) dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG)) onClose() diff --git a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx index 5cb4a402..13313a54 100644 --- a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx @@ -6,6 +6,7 @@ import Block from 'src/components/layout/Block' import Paragraph from 'src/components/layout/Paragraph' import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions' import { useEffect, useState } from 'react' +import { getValidAddressBookName } from 'src/logic/addressBook/utils' type OwnerAddressTableCellProps = { address: string @@ -34,7 +35,7 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl {showLinks ? (
- {!userName || userName === 'UNKNOWN' ? null : userName} + {userName && getValidAddressBookName(userName)}
) : ( diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index 446a5c0f..1293ea4e 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -30,8 +30,8 @@ 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' -import { AddressBookCollection } from 'src/logic/addressBook/store/reducer/addressBook' export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn' export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn' @@ -42,7 +42,7 @@ export const OWNERS_ROW_TEST_ID = 'owners-row' const useStyles = makeStyles(styles) type Props = { - addressBook: unknown + addressBook: AddressBookState granted: boolean owners: List } @@ -84,7 +84,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) - const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook as AddressBookCollection, owners) + const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners) const ownerData = getOwnerData(ownersAdbk) return ( diff --git a/src/routes/safe/components/Settings/index.tsx b/src/routes/safe/components/Settings/index.tsx index 50fdb843..bf7281aa 100644 --- a/src/routes/safe/components/Settings/index.tsx +++ b/src/routes/safe/components/Settings/index.tsx @@ -23,7 +23,7 @@ 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 { getAddressBook } from 'src/logic/addressBook/store/selectors' +import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' import { safeNeedsUpdateSelector, safeOwnersSelector } from 'src/logic/safe/store/selectors' @@ -42,7 +42,7 @@ const Settings: React.FC = () => { const owners = useSelector(safeOwnersSelector) const needsUpdate = useSelector(safeNeedsUpdateSelector) const granted = useSelector(grantedSelector) - const addressBook = useSelector(getAddressBook) + const addressBook = useSelector(addressBookSelector) const handleChange = (menuOptionIndex) => () => { setState((prevState) => ({ ...prevState, menuOptionIndex })) diff --git a/src/store/index.ts b/src/store/index.ts index cd67c3f6..0b5d7a8d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,7 +6,6 @@ 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 { AddressBookReducerMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID, @@ -19,8 +18,10 @@ import currencyValues, { CURRENCY_VALUES_KEY, CurrencyValuesState, } from 'src/logic/currencyValues/store/reducer/currencyValues' -import { CurrentSessionState } from 'src/logic/currentSession/store/reducer/currentSession' -import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession' +import currentSession, { + CURRENT_SESSION_REDUCER_ID, + CurrentSessionState, +} from 'src/logic/currentSession/store/reducer/currentSession' import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications' import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens' import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher' @@ -38,7 +39,8 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' -import allTransactions, { TRANSACTIONS, TransactionsState } from 'src/logic/safe/store/reducer/allTransactions' +import allTransactions, { TRANSACTIONS, TransactionsState } from '../logic/safe/store/reducer/allTransactions' +import { AddressBookState } from 'src/logic/addressBook/model/addressBook' export const history = createHashHistory() @@ -86,7 +88,7 @@ export type AppReduxState = CombinedState<{ [NOTIFICATIONS_REDUCER_ID]: Map [CURRENCY_VALUES_KEY]: CurrencyValuesState [COOKIES_REDUCER_ID]: Map - [ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap + [ADDRESS_BOOK_REDUCER_ID]: AddressBookState [CURRENT_SESSION_REDUCER_ID]: CurrentSessionState [TRANSACTIONS]: TransactionsState router: RouterState