mirror of
https://github.com/status-im/safe-react.git
synced 2025-01-09 09:42:07 +00:00
[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:
parent
82519b84b6
commit
2308c1f567
@ -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)
|
||||
}
|
||||
}
|
||||
|
133
src/logic/addressBook/utils/v2-migration.ts
Normal file
133
src/logic/addressBook/utils/v2-migration.ts
Normal 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
|
@ -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',
|
||||
}
|
||||
|
@ -1,25 +1,16 @@
|
||||
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) => {
|
||||
safes.forEach((safeProps) => {
|
||||
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Error while getting Safes from storage:', err)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user