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)