[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
This commit is contained in:
katspaugh 2021-05-27 15:52:15 +02:00 committed by GitHub
parent 82519b84b6
commit 2308c1f567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 154 additions and 170 deletions

View File

@ -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<string, Overwrite<SafeRecordProps, { name: string }>>
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<AddressBookEntry, 'chainId'>[])
// 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)
}
}

View File

@ -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<string, any>
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<AddressBookEntry, 'chainId'>[])
// 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

View File

@ -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',
}

View File

@ -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<void> => {
try {
const safes = await loadFromStorage<Record<string, SafeRecordProps>>(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()

View File

@ -18,6 +18,11 @@ export const saveSafes = async (safes: StoredSafes): Promise<void> => {
}
}
export const getLocalSafes = async (): Promise<SafeRecordProps[] | undefined> => {
const storedSafes = await loadStoredSafes()
return storedSafes ? Object.values(storedSafes) : undefined
}
export const getLocalSafe = async (safeAddress: string): Promise<SafeRecordProps | undefined> => {
const storedSafes = await loadStoredSafes()
return storedSafes?.[safeAddress]

View File

@ -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<unknown>): Store =>
createStore(reducers, localState, finalCreateStore)