feat: redux migration (#6830)

* feat: start working on redux migrations

* feat: fix migations and add tests

* feat: fix persistence and improve tests

* fix: tests

* fix: rename test file so it doesnt run in jest

* fix: tests

* fix: lint

* feat: indexedDB

* fix: e2e tests

* fix: address some comments

* fix: update legacy migrations

* fix: fix rehydrations

* fix: remove PersistGate and fix e2e tests

* fix: add comment to helper function
This commit is contained in:
eddie 2023-08-16 10:56:06 -07:00 committed by GitHub
parent 69ae42f285
commit 38cce46c7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 661 additions and 250 deletions

@ -3,8 +3,8 @@ import 'cypress-hardhat/lib/browser'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import { FeatureFlag } from '../../src/featureFlags'
import { UserState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
import { initialState, UserState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE, setInitialUserState } from '../utils/user-state'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@ -54,18 +54,12 @@ Cypress.Commands.overwrite(
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
// We want to test from a clean state, so we clear the local storage (which clears redux).
win.localStorage.clear()
// Set initial user state.
win.localStorage.setItem(
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
JSON.stringify({
setInitialUserState(win, {
...initialState,
hideUniswapWalletBanner: true,
...CONNECTED_WALLET_USER_STATE,
...(options?.userState ?? {}),
})
)
// Set feature flags, if configured.
if (options?.featureFlags) {

@ -4,3 +4,31 @@ import { UserState } from '../../src/state/user/reducer'
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: ConnectionType.INJECTED }
export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: undefined }
/**
* This sets the initial value of the "user" slice in IndexedDB.
* Other persisted slices are not set, so they will be filled with their respective initial values
* when the app runs.
*/
export function setInitialUserState(win: Cypress.AUTWindow, initialUserState: any) {
win.indexedDB.deleteDatabase('redux')
const dbRequest = win.indexedDB.open('redux')
dbRequest.onsuccess = function () {
const db = dbRequest.result
const transaction = db.transaction('keyvaluepairs', 'readwrite')
const store = transaction.objectStore('keyvaluepairs')
store.put(
{
user: initialUserState,
},
'persist:interface'
)
}
dbRequest.onupgradeneeded = function () {
const db = dbRequest.result
db.createObjectStore('keyvaluepairs')
}
}

@ -143,6 +143,7 @@
"terser-webpack-plugin": "^5.3.9",
"ts-jest": "^29.1.1",
"ts-transform-graphql-tag": "^0.2.1",
"tsafe": "^1.6.4",
"typechain": "^5.0.0",
"typescript": "^4.9.4",
"webpack": "^5.88.2",
@ -235,6 +236,7 @@
"inter-ui": "^3.13.1",
"jotai": "^1.3.7",
"jsbi": "^3.1.4",
"localforage": "^1.10.0",
"make-plural": "^7.0.0",
"ms": "^2.1.3",
"multicodec": "^3.0.1",
@ -265,7 +267,7 @@
"react-window-infinite-loader": "^1.0.8",
"rebass": "^4.0.7",
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.3.1",
"redux-persist": "^6.0.0",
"statsig-react": "^1.22.0",
"styled-components": "^5.3.5",
"tiny-invariant": "^1.2.0",

@ -1,7 +1,6 @@
import { Connector } from '@web3-react/types'
import { gnosisSafeConnection, networkConnection } from 'connection'
import { getConnection } from 'connection'
import { Connection } from 'connection/types'
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
@ -22,22 +21,24 @@ export default function useEagerlyConnect() {
const dispatch = useAppDispatch()
const selectedWallet = useAppSelector((state) => state.user.selectedWallet)
let selectedConnection: Connection | undefined
if (selectedWallet) {
try {
selectedConnection = getConnection(selectedWallet)
} catch {
dispatch(updateSelectedWallet({ wallet: undefined }))
}
}
const rehydrated = useAppSelector((state) => state._persist.rehydrated)
useEffect(() => {
if (!selectedWallet) return
try {
const selectedConnection = getConnection(selectedWallet)
connect(gnosisSafeConnection.connector)
connect(networkConnection.connector)
if (selectedConnection) {
connect(selectedConnection.connector)
} // The dependency list is empty so this is only run once on mount
}, []) // eslint-disable-line react-hooks/exhaustive-deps
}
} catch {
// only clear the persisted wallet type if it failed to connect.
if (rehydrated) {
dispatch(updateSelectedWallet({ wallet: undefined }))
}
return
}
}, [dispatch, rehydrated, selectedWallet])
}

@ -3,8 +3,8 @@ import { sendAnalyticsEvent } from 'analytics'
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { AppState } from 'state/reducer'
import { AppState } from '../types'
import {
addPopup,
ApplicationModal,

@ -47,7 +47,7 @@ export enum ApplicationModal {
UNISWAP_NFT_AIRDROP_CLAIM,
}
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
export type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
export interface ApplicationState {
readonly chainId: number | null

@ -6,11 +6,11 @@ import JSBI from 'jsbi'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useCallback } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { AppState } from 'state/reducer'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useV2Pair } from '../../hooks/useV2Pairs'
import { useTokenBalances } from '../connection/hooks'
import { AppState } from '../types'
import { Field, typeInput } from './actions'
export function useBurnState(): AppState['burn'] {

@ -2,7 +2,7 @@ import { createReducer } from '@reduxjs/toolkit'
import { Field, typeInput } from './actions'
interface BurnState {
export interface BurnState {
readonly independentField: Field
readonly typedValue: string
}

@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks'
import { PositionDetails } from 'types/position'
import { unwrappedToken } from 'utils/unwrappedToken'
import { AppState } from '../../types'
import { AppState } from '../../reducer'
import { selectPercent } from './actions'
export function useBurnV3State(): AppState['burnV3'] {

@ -2,7 +2,7 @@ import { createReducer } from '@reduxjs/toolkit'
import { selectPercent } from './actions'
interface BurnV3State {
export interface BurnV3State {
readonly percent: number
}

@ -1,16 +1,14 @@
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query/react'
import { load, save } from 'redux-localstorage-simple'
import { isTestEnv } from 'utils/env'
import { persistStore } from 'redux-persist'
import { updateVersion } from './global/actions'
import { sentryEnhancer } from './logging'
import reducer from './reducer'
import { routingApi } from './routing/slice'
const PERSISTED_KEYS: string[] = ['user', 'transactions', 'signatures', 'lists']
const store = configureStore({
export function createDefaultStore() {
return configureStore({
reducer,
enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer),
middleware: (getDefaultMiddleware) =>
@ -21,15 +19,23 @@ const store = configureStore({
// because we are not adding it into any persisted store that requires serialization (e.g. localStorage)
ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'],
ignoredPaths: [routingApi.reducerPath],
ignoredActions: [
// ignore the redux-persist actions
'persist/PERSIST',
'persist/REHYDRATE',
'persist/PURGE',
'persist/FLUSH',
],
},
}).concat(routingApi.middleware),
})
.concat(routingApi.middleware)
.concat(save({ states: PERSISTED_KEYS, debounce: 1000 })),
preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }),
})
}
store.dispatch(updateVersion())
const store = createDefaultStore()
export const persistor = persistStore(store)
setupListeners(store.dispatch)
store.dispatch(updateVersion())
export default store

@ -1,10 +1,10 @@
import { TokenAddressMap, tokensToChainTokenMap } from 'lib/hooks/useTokenList/utils'
import { useMemo } from 'react'
import { useAppSelector } from 'state/hooks'
import { AppState } from 'state/reducer'
import sortByListPriority from 'utils/listSort'
import BROKEN_LIST from '../../constants/tokenLists/broken.tokenlist.json'
import { AppState } from '../types'
import { DEFAULT_ACTIVE_LIST_URLS, UNSUPPORTED_LIST_URLS } from './../../constants/lists'
type Mutable<T> = {

@ -1,8 +1,8 @@
import tokenSafetyLookup from 'constants/tokenSafetyLookup'
import { createStore, Store } from 'redux'
import { updateVersion } from 'state/global/actions'
import { DEFAULT_LIST_OF_LISTS } from '../../constants/lists'
import tokenSafetyLookup from '../../constants/tokenSafetyLookup'
import { updateVersion } from '../global/actions'
import { acceptListUpdate, addList, fetchTokenList, removeList } from './actions'
import reducer, { ListsState } from './reducer'

@ -30,7 +30,7 @@ const NEW_LIST_STATE: ListState = {
type Mutable<T> = { -readonly [P in keyof T]: T[P] extends ReadonlyArray<infer U> ? U[] : T[P] }
const initialState: ListsState = {
export const initialState: ListsState = {
lastInitializedDefaultListOfLists: DEFAULT_LIST_OF_LISTS,
byUrl: {
...DEFAULT_LIST_OF_LISTS.reduce<Mutable<ListsState['byUrl']>>((memo, listUrl) => {

@ -1,7 +1,7 @@
import * as Sentry from '@sentry/react'
import noop from 'utils/noop'
import { AppState } from './types'
import { AppState } from './reducer'
/* Utility type to mark all properties of a type as optional */
type DeepPartial<T> = T extends object

@ -3,7 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { filterToKey, Log } from './utils'
interface LogsState {
export interface LogsState {
[chainId: number]: {
[filterKey: string]: {
listeners: number

@ -0,0 +1,135 @@
import { Store } from '@reduxjs/toolkit'
import { persistStore } from 'redux-persist'
import { createDefaultStore } from 'state'
import { initialState as initialListsState } from './lists/reducer'
import { initialState as initialSignaturesState } from './signatures/reducer'
import { initialState as initialTransactionsState } from './transactions/reducer'
import { initialState as initialUserState } from './user/reducer'
const defaultState = {
lists: {},
transactions: {},
user: {},
_persist: {
rehydrated: true,
version: 0,
},
application: {
chainId: null,
fiatOnramp: {
availabilityChecked: false,
available: false,
},
openModal: null,
popupList: [],
},
burn: {
independentField: 'LIQUIDITY_PERCENT',
typedValue: '0',
},
burnV3: {
percent: 0,
},
logs: {},
mint: {
independentField: 'CURRENCY_A',
leftRangeTypedValue: '',
otherTypedValue: '',
rightRangeTypedValue: '',
startPriceTypedValue: '',
typedValue: '',
},
mintV3: {
independentField: 'CURRENCY_A',
leftRangeTypedValue: '',
rightRangeTypedValue: '',
startPriceTypedValue: '',
typedValue: '',
},
multicall: {
callResults: {},
},
wallets: {
connectedWallets: [],
switchingChain: false,
},
}
describe('redux migrations', () => {
let store: Store
beforeEach(() => {
localStorage.clear()
// Re-create the store before each test so it starts with undefined state.
store = createDefaultStore()
})
it('clears legacy redux_localstorage_simple values during the initial migration', async () => {
localStorage.setItem(
'redux_localstorage_simple_transactions',
JSON.stringify({ 1: { test: { info: 'transactions' } } })
)
localStorage.setItem('redux_localstorage_simple_user', JSON.stringify({ test: 'user' }))
localStorage.setItem('redux_localstorage_simple_lists', JSON.stringify({ test: 'lists' }))
localStorage.setItem('redux_localstorage_simple_signatures', JSON.stringify({ test: 'signatures' }))
persistStore(store)
// wait for the migration to complete
await new Promise((resolve) => setTimeout(resolve, 0))
expect(localStorage.getItem('redux_localstorage_simple_transactions')).toBeNull()
expect(localStorage.getItem('redux_localstorage_simple_user')).toBeNull()
expect(localStorage.getItem('redux_localstorage_simple_lists')).toBeNull()
expect(localStorage.getItem('redux_localstorage_simple_signatures')).toBeNull()
const state = store.getState()
expect(state).toMatchObject({
...defaultState,
// These are migrated values.
lists: {
test: 'lists',
},
transactions: {
1: {
test: { info: 'transactions' },
},
},
user: {
test: 'user',
},
signatures: {
test: 'signatures',
},
})
})
it('initial state with no previous persisted state', async () => {
persistStore(store)
// wait for the migration to complete
await new Promise((resolve) => setTimeout(resolve, 0))
const state = store.getState()
expect(state).toMatchObject(defaultState)
})
it('migrates from a previous version of the state type', async () => {
localStorage.setItem(
'persist:interface',
JSON.stringify({
user: { ...initialUserState, test: 'user' },
transactions: initialTransactionsState,
lists: initialListsState,
signatures: initialSignaturesState,
_persist: { version: -1 },
})
)
persistStore(store)
// wait for the migration to complete
await new Promise((resolve) => setTimeout(resolve, 0))
const state = store.getState()
expect(state).toMatchObject(defaultState)
})
})

35
src/state/migrations.ts Normal file

@ -0,0 +1,35 @@
import { createMigrate, MigrationManifest, PersistedState, PersistMigrate } from 'redux-persist'
import { MigrationConfig } from 'redux-persist/es/createMigrate'
import { migration0 } from './migrations/0'
import { legacyLocalStorageMigration } from './migrations/legacy'
/**
* These run once per state re-hydration when a version mismatch is detected.
* Keep them as lightweight as possible.
*
* Migration functions should not assume that any value exists in localStorage previously,
* because a user may be visiting the site for the first time or have cleared their localStorage.
*/
// The target version number is the key
export const migrations: MigrationManifest = {
0: migration0,
}
// We use a custom migration function for the initial state, because redux-persist
// skips migration if there is no initial state, but we want to migrate
// previous persisted state from redux-localstorage-simple.
export function customCreateMigrate(migrations: MigrationManifest, options: MigrationConfig): PersistMigrate {
const defaultMigrate = createMigrate(migrations, options)
return async (state: PersistedState, currentVersion: number) => {
if (state === undefined) {
// If no state exists, run the legacy migration to set initial state
state = await legacyLocalStorageMigration()
}
// Otherwise, use the default migration process
return defaultMigrate(state, currentVersion)
}
}

10
src/state/migrations/0.ts Normal file

@ -0,0 +1,10 @@
import { PersistedState } from 'redux-persist'
/**
* Initial migration as a proof of concept.
*
* Legacy migration from redux-localstorage-simple happens in legacy.ts
*/
export const migration0 = (state: PersistedState) => {
return state
}

@ -0,0 +1,128 @@
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
import { persistor } from 'state'
import { initialState as initialListsState } from '../lists/reducer'
import { RouterPreference } from '../routing/types'
import { TransactionState } from '../transactions/reducer'
import { initialState as initialTransactionsState } from '../transactions/reducer'
import { UserState } from '../user/reducer'
import { initialState as initialUserState } from '../user/reducer'
import { SlippageTolerance } from '../user/types'
const currentTimestamp = () => new Date().getTime()
function tryParseOldState<T>(value: string | null, fallback: T): T {
try {
return value ? JSON.parse(value) : fallback
} catch (e) {
return fallback
}
}
/**
* These functions handle all migrations that existed before we started tracking version numbers.
*/
export const legacyLocalStorageMigration = async () => {
const oldTransactions = localStorage.getItem('redux_localstorage_simple_transactions')
const oldUser = localStorage.getItem('redux_localstorage_simple_user')
const oldLists = localStorage.getItem('redux_localstorage_simple_lists')
const oldSignatures = localStorage.getItem('redux_localstorage_simple_signatures')
const newTransactions = tryParseOldState(oldTransactions, initialTransactionsState)
const newUser = tryParseOldState(oldUser, initialUserState)
const newLists = tryParseOldState(oldLists, initialListsState)
const newSignatures = tryParseOldState(oldSignatures, {})
const result = {
user: legacyUserMigrations(newUser),
transactions: legacyTransactionMigrations(newTransactions),
lists: newLists,
signatures: newSignatures,
_persist: { version: 0, rehydrated: true },
}
await persistor.flush()
localStorage.removeItem('redux_localstorage_simple_transactions')
localStorage.removeItem('redux_localstorage_simple_user')
localStorage.removeItem('redux_localstorage_simple_lists')
localStorage.removeItem('redux_localstorage_simple_signatures')
return result
}
function legacyTransactionMigrations(state: any): TransactionState {
// Make a copy of the object so we can mutate it.
const result = JSON.parse(JSON.stringify(state))
// in case there are any transactions in the store with the old format, remove them
Object.keys(result).forEach((chainId) => {
const chainTransactions = result[chainId as unknown as number]
Object.keys(chainTransactions).forEach((hash) => {
if (!('info' in chainTransactions[hash])) {
// clear old transactions that don't have the right format
delete chainTransactions[hash]
}
})
})
return result
}
function legacyUserMigrations(state: any): UserState {
// Make a copy of the object so we can mutate it.
const result = JSON.parse(JSON.stringify(state))
// If `selectedWallet` is a WalletConnect v1 wallet, reset to default.
if (result.selectedWallet) {
const selectedWallet = result.selectedWallet as string
if (selectedWallet === 'UNIWALLET' || selectedWallet === 'UNISWAP_WALLET' || selectedWallet === 'WALLET_CONNECT') {
delete result.selectedWallet
}
}
// If `userSlippageTolerance` is not present or its value is invalid, reset to default
if (
typeof result.userSlippageTolerance !== 'number' ||
!Number.isInteger(result.userSlippageTolerance) ||
result.userSlippageTolerance < 0 ||
result.userSlippageTolerance > 5000
) {
result.userSlippageTolerance = SlippageTolerance.Auto
} else {
if (
!result.userSlippageToleranceHasBeenMigratedToAuto &&
[10, 50, 100].indexOf(result.userSlippageTolerance) !== -1
) {
result.userSlippageTolerance = SlippageTolerance.Auto
result.userSlippageToleranceHasBeenMigratedToAuto = true
}
}
// If `userDeadline` is not present or its value is invalid, reset to default
if (
typeof result.userDeadline !== 'number' ||
!Number.isInteger(result.userDeadline) ||
result.userDeadline < 60 ||
result.userDeadline > 180 * 60
) {
result.userDeadline = DEFAULT_DEADLINE_FROM_NOW
}
// If `userRouterPreference` is not present, reset to default
if (typeof result.userRouterPreference !== 'string') {
result.userRouterPreference = RouterPreference.API
}
// If `userRouterPreference` is `AUTO`, migrate to `API`
if ((result.userRouterPreference as string) === 'auto') {
result.userRouterPreference = RouterPreference.API
}
//If `buyFiatFlowCompleted` is present, delete it using filtering
if ('buyFiatFlowCompleted' in result) {
//ignoring due to type errors occuring since we now remove this state
//@ts-ignore
delete result.buyFiatFlowCompleted
}
result.lastUpdateVersionTimestamp = currentTimestamp()
return result
}

@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { PairState, useV2Pair } from '../../hooks/useV2Pairs'
import { useCurrencyBalances } from '../connection/hooks'
import { AppState } from '../types'
import { AppState } from '../reducer'
import { Field, typeInput } from './actions'
const ZERO = JSBI.BigInt(0)

@ -23,7 +23,7 @@ import { getTickToPrice } from 'utils/getTickToPrice'
import { BIG_INT_ZERO } from '../../../constants/misc'
import { PoolState } from '../../../hooks/usePools'
import { useCurrencyBalances } from '../../connection/hooks'
import { AppState } from '../../types'
import { AppState } from '../../reducer'
import {
Bound,
Field,

@ -10,9 +10,9 @@ import {
typeStartPriceInput,
} from './actions'
type FullRange = true
export type FullRange = true
interface MintState {
export interface MintState {
readonly independentField: Field
readonly typedValue: string
readonly startPriceTypedValue: string // for the case when there's no liquidity

@ -1,10 +1,15 @@
import { combineReducers } from '@reduxjs/toolkit'
import multicall from 'lib/state/multicall'
import localForage from 'localforage'
import { PersistConfig, persistReducer } from 'redux-persist'
import { isDevelopmentEnv } from 'utils/env'
import application from './application/reducer'
import burn from './burn/reducer'
import burnV3 from './burn/v3/reducer'
import lists from './lists/reducer'
import logs from './logs/slice'
import { customCreateMigrate, migrations } from './migrations'
import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer'
import { routingApi } from './routing/slice'
@ -13,18 +18,45 @@ import transactions from './transactions/reducer'
import user from './user/reducer'
import wallets from './wallets/reducer'
export default {
application,
const persistedReducers = {
user,
transactions,
signatures,
lists,
}
const appReducer = combineReducers({
application,
wallets,
mint,
mintV3,
burn,
burnV3,
multicall: multicall.reducer,
lists,
logs,
[routingApi.reducerPath]: routingApi.reducer,
...persistedReducers,
})
export type AppState = ReturnType<typeof appReducer>
const persistConfig: PersistConfig<AppState> = {
key: 'interface',
version: 0, // see migrations.ts for more details about this version
storage: localForage.createInstance({
name: 'redux',
}),
migrate: customCreateMigrate(migrations, { debug: false }),
whitelist: Object.keys(persistedReducers),
throttle: 1000, // ms
serialize: false,
// The typescript definitions are wrong - we need this to be false for unserialized storage to work.
// We need unserialized storage for inspectable db entries for debugging.
// @ts-ignore
deserialize: false,
debug: isDevelopmentEnv(),
}
const persistedReducer = persistReducer(persistConfig, appReducer)
export default persistedReducer

@ -0,0 +1,188 @@
import { ChainId } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { ConnectionType } from 'connection/types'
import { SupportedLocale } from 'constants/locales'
import multicall from 'lib/state/multicall'
import { CombinedState } from 'redux'
import { assert, Equals } from 'tsafe'
import { ApplicationModal, ApplicationState, PopupList } from './application/reducer'
import { Field as BurnField } from './burn/actions'
import { BurnState } from './burn/reducer'
import { BurnV3State } from './burn/v3/reducer'
import { ListsState } from './lists/reducer'
import { LogsState } from './logs/slice'
import { Log } from './logs/utils'
import { Field } from './mint/actions'
import { MintState } from './mint/reducer'
import { Field as FieldV3 } from './mint/v3/actions'
import { FullRange, MintState as MintV3State } from './mint/v3/reducer'
import { AppState } from './reducer'
import { routingApi } from './routing/slice'
import { RouterPreference } from './routing/types'
import { SignatureState } from './signatures/reducer'
import { TransactionState } from './transactions/reducer'
import { TransactionDetails } from './transactions/types'
import { UserState } from './user/reducer'
import { SerializedPair, SerializedToken, SlippageTolerance } from './user/types'
import { WalletState } from './wallets/reducer'
import { Wallet } from './wallets/types'
/**
* WARNING:
* Any changes made to the types of the Redux store could potentially require a migration.
*
* If you're making a change that alters the structure or types of the Redux state,
* consider whether existing state stored in users' browsers will still be compatible
* with the new types.
*
* If compatibility could be broken, you may need to create a migration
* function that can convert the existing state into a format that's compatible with
* the new types, or otherwise adjust the user's persisted state in some way
* to prevent undesirable behavior.
*
* This migration function should be added to the `migrations` object
* in our Redux store configuration.
*
* If no migration is needed, just update the expected types here to fix the typecheck.
*/
type ExpectedAppState = CombinedState<{
user: UserState
transactions: TransactionState
signatures: SignatureState
lists: ListsState
application: ApplicationState
wallets: WalletState
mint: MintState
mintV3: MintV3State
burn: BurnState
burnV3: BurnV3State
multicall: ReturnType<typeof multicall.reducer>
logs: LogsState
[routingApi.reducerPath]: ReturnType<typeof routingApi.reducer>
}>
assert<Equals<AppState, ExpectedAppState>>()
interface ExpectedUserState {
selectedWallet?: ConnectionType
lastUpdateVersionTimestamp?: number
userLocale: SupportedLocale | null
userRouterPreference: RouterPreference
userHideClosedPositions: boolean
userSlippageTolerance: number | SlippageTolerance.Auto
userSlippageToleranceHasBeenMigratedToAuto: boolean
userDeadline: number
tokens: {
[chainId: number]: {
[address: string]: SerializedToken
}
}
pairs: {
[chainId: number]: {
[key: string]: SerializedPair
}
}
timestamp: number
URLWarningVisible: boolean
hideBaseWalletBanner: boolean
showSurveyPopup?: boolean
disabledUniswapX?: boolean
}
assert<Equals<UserState, ExpectedUserState>>()
interface ExpectedTransactionState {
[chainId: number]: {
[txHash: string]: TransactionDetails
}
}
assert<Equals<TransactionState, ExpectedTransactionState>>()
interface ExpectedListsState {
readonly byUrl: {
readonly [url: string]: {
readonly current: TokenList | null
readonly pendingUpdate: TokenList | null
readonly loadingRequestId: string | null
readonly error: string | null
}
}
readonly lastInitializedDefaultListOfLists?: string[]
}
assert<Equals<ListsState, ExpectedListsState>>()
interface ExpectedApplicationState {
readonly chainId: number | null
readonly fiatOnramp: { available: boolean; availabilityChecked: boolean }
readonly openModal: ApplicationModal | null
readonly popupList: PopupList
}
assert<Equals<ApplicationState, ExpectedApplicationState>>()
interface ExpectedWalletState {
connectedWallets: Wallet[]
switchingChain: ChainId | false
}
assert<Equals<WalletState, ExpectedWalletState>>()
interface ExpectedMintState {
readonly independentField: Field
readonly typedValue: string
readonly otherTypedValue: string
readonly startPriceTypedValue: string
readonly leftRangeTypedValue: string
readonly rightRangeTypedValue: string
}
assert<Equals<MintState, ExpectedMintState>>()
interface ExpectedMintV3State {
readonly independentField: FieldV3
readonly typedValue: string
readonly startPriceTypedValue: string
readonly leftRangeTypedValue: string | FullRange
readonly rightRangeTypedValue: string | FullRange
}
assert<Equals<MintV3State, ExpectedMintV3State>>()
interface ExpectedBurnState {
readonly independentField: BurnField
readonly typedValue: string
}
assert<Equals<BurnState, ExpectedBurnState>>()
interface ExpectedBurnV3State {
readonly percent: number
}
assert<Equals<BurnV3State, ExpectedBurnV3State>>()
interface ExpectedLogsState {
[chainId: number]: {
[filterKey: string]: {
listeners: number
fetchingBlockNumber?: number
results?:
| {
blockNumber: number
logs: Log[]
error?: undefined
}
| {
blockNumber: number
logs?: undefined
error: true
}
}
}
}
assert<Equals<LogsState, ExpectedLogsState>>()

@ -1,7 +1,6 @@
import { ChainId } from '@uniswap/sdk-core'
import { createStore, Store } from 'redux'
import { updateVersion } from '../global/actions'
import reducer, {
addTransaction,
cancelTransaction,
@ -20,32 +19,6 @@ describe('transaction reducer', () => {
store = createStore(reducer, initialState)
})
describe('updateVersion', () => {
it('clears old format transactions that do not have info', () => {
store = createStore(reducer, {
1: {
abc: {
hash: 'abc',
} as any,
},
})
store.dispatch(updateVersion())
expect(store.getState()[ChainId.MAINNET]['abc']).toBeUndefined()
})
it('keeps old format transactions that do have info', () => {
store = createStore(reducer, {
1: {
abc: {
hash: 'abc',
info: {},
} as any,
},
})
store.dispatch(updateVersion())
expect(store.getState()[ChainId.MAINNET]['abc']).toBeTruthy()
})
})
describe('addTransaction', () => {
it('adds the transaction', () => {
const beforeTime = new Date().getTime()

@ -1,7 +1,6 @@
import { createSlice } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk-core'
import { updateVersion } from '../global/actions'
import { SerializableTransactionReceipt, TransactionDetails, TransactionInfo } from './types'
// TODO(WEB-2053): update this to be a map of account -> chainId -> txHash -> TransactionDetails
@ -80,20 +79,6 @@ const transactionSlice = createSlice({
}
},
},
extraReducers: (builder) => {
builder.addCase(updateVersion, (transactions) => {
// in case there are any transactions in the store with the old format, remove them
Object.keys(transactions).forEach((chainId) => {
const chainTransactions = transactions[chainId as unknown as number]
Object.keys(chainTransactions).forEach((hash) => {
if (!('info' in chainTransactions[hash])) {
// clear old transactions that don't have the right format
delete chainTransactions[hash]
}
})
})
})
},
})
export const {

@ -1,10 +0,0 @@
import { Reducer } from '@reduxjs/toolkit'
import reducer from './reducer'
/* Utility type to extract state type out of a @reduxjs/toolkit Reducer type */
type GetState<T> = T extends Reducer<infer State> ? State : never
export type AppState = {
[K in keyof typeof reducer]: GetState<(typeof reducer)[K]>
}

@ -12,7 +12,7 @@ import { UserAddedToken } from 'types/tokens'
import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants/routing'
import { useDefaultActiveTokens } from '../../hooks/Tokens'
import { AppState } from '../types'
import { AppState } from '../reducer'
import {
addSerializedPair,
addSerializedToken,

@ -1,8 +1,6 @@
import { createStore, Store } from 'redux'
import { RouterPreference } from 'state/routing/types'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { updateVersion } from '../global/actions'
import reducer, {
addSerializedPair,
addSerializedToken,
@ -16,7 +14,6 @@ import reducer, {
updateUserSlippageTolerance,
UserState,
} from './reducer'
import { SlippageTolerance } from './types'
function buildSerializedPair(token0Address: string, token1Address: string, chainId: number) {
return {
@ -38,36 +35,6 @@ describe('swap reducer', () => {
store = createStore(reducer, initialState)
})
describe('updateVersion', () => {
it('has no timestamp originally', () => {
expect(store.getState().lastUpdateVersionTimestamp).toBeUndefined()
})
it('sets the lastUpdateVersionTimestamp', () => {
const time = new Date().getTime()
store.dispatch(updateVersion())
expect(store.getState().lastUpdateVersionTimestamp).toBeGreaterThanOrEqual(time)
})
it('sets allowed slippage and deadline', () => {
store = createStore(reducer, {
...initialState,
userDeadline: undefined,
userSlippageTolerance: undefined,
} as any)
store.dispatch(updateVersion())
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
expect(store.getState().userSlippageTolerance).toEqual(SlippageTolerance.Auto)
})
it('sets allowed slippage and deadline to auto', () => {
store = createStore(reducer, {
...initialState,
userSlippageTolerance: 10,
userSlippageToleranceHasBeenMigratedToAuto: undefined,
} as any)
store.dispatch(updateVersion())
expect(store.getState().userSlippageToleranceHasBeenMigratedToAuto).toEqual(true)
})
})
describe('updateSelectedWallet', () => {
it('updates the selected wallet', () => {
store.dispatch(updateSelectedWallet({ wallet: 'metamask' }))

@ -4,7 +4,6 @@ import { ConnectionType } from '../../connection/types'
import { SupportedLocale } from '../../constants/locales'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { RouterPreference } from '../../state/routing/types'
import { updateVersion } from '../global/actions'
import { SerializedPair, SerializedToken, SlippageTolerance } from './types'
const currentTimestamp = () => new Date().getTime()
@ -124,85 +123,6 @@ const userSlice = createSlice({
state.timestamp = currentTimestamp()
},
},
extraReducers: (builder) => {
// After adding a new property to the state, its value will be `undefined` (instead of the default)
// for all existing users with a previous version of the state in their localStorage.
// In order to avoid this, we need to set a default value for each new property manually during hydration.
builder.addCase(updateVersion, (state) => {
// If `selectedWallet` is a WalletConnect v1 wallet, reset to default.
if (state.selectedWallet) {
const selectedWallet = state.selectedWallet as string
if (
selectedWallet === 'UNIWALLET' ||
selectedWallet === 'UNISWAP_WALLET' ||
selectedWallet === 'WALLET_CONNECT'
) {
delete state.selectedWallet
}
}
// If `userSlippageTolerance` is not present or its value is invalid, reset to default
if (
typeof state.userSlippageTolerance !== 'number' ||
!Number.isInteger(state.userSlippageTolerance) ||
state.userSlippageTolerance < 0 ||
state.userSlippageTolerance > 5000
) {
state.userSlippageTolerance = SlippageTolerance.Auto
} else {
if (
!state.userSlippageToleranceHasBeenMigratedToAuto &&
[10, 50, 100].indexOf(state.userSlippageTolerance) !== -1
) {
state.userSlippageTolerance = SlippageTolerance.Auto
state.userSlippageToleranceHasBeenMigratedToAuto = true
}
}
// If `userDeadline` is not present or its value is invalid, reset to default
if (
typeof state.userDeadline !== 'number' ||
!Number.isInteger(state.userDeadline) ||
state.userDeadline < 60 ||
state.userDeadline > 180 * 60
) {
state.userDeadline = DEFAULT_DEADLINE_FROM_NOW
}
// If `userRouterPreference` is not present, reset to default
if (typeof state.userRouterPreference !== 'string') {
state.userRouterPreference = RouterPreference.API
}
// If `userRouterPreference` is `AUTO`, migrate to `API`
if ((state.userRouterPreference as string) === 'auto') {
state.userRouterPreference = RouterPreference.API
}
//If `buyFiatFlowCompleted` is present, delete it using filtering
if ('buyFiatFlowCompleted' in state) {
//ignoring due to type errors occuring since we now remove this state
//@ts-ignore
delete state.buyFiatFlowCompleted
}
// If `buyFiatFlowCompleted` is present, delete it using filtering
if ('buyFiatFlowCompleted' in state) {
//ignoring due to type errors occuring since we now remove this state
//@ts-ignore
delete state.buyFiatFlowCompleted
}
//If `buyFiatFlowCompleted` is present, delete it using filtering
if ('buyFiatFlowCompleted' in state) {
//ignoring due to type errors occuring since we now remove this state
//@ts-ignore
delete state.buyFiatFlowCompleted
}
state.lastUpdateVersionTimestamp = currentTimestamp()
})
},
})
export const {

@ -4,7 +4,7 @@ import { shallowEqual } from 'react-redux'
import { Wallet } from './types'
interface WalletState {
export interface WalletState {
// Used to track wallets that have been connected by the user in current session, and remove them when deliberately disconnected.
// Used to compute is_reconnect event property for analytics
connectedWallets: Wallet[]

@ -12952,6 +12952,11 @@ image-q@^1.1.1:
resolved "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz"
integrity sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immer@^9.0.21, immer@^9.0.6, immer@^9.0.7:
version "9.0.21"
resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
@ -14896,6 +14901,13 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
dependencies:
immediate "~3.0.5"
lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
@ -14999,6 +15011,13 @@ loader-utils@^3.2.0:
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576"
integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==
localforage@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
dependencies:
lie "3.1.1"
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@ -15298,11 +15317,6 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
merge@2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/merge/-/merge-2.1.0.tgz"
integrity sha512-TcuhVDV+e6X457MQAm7xIb19rWhZuEDEho7RrwxMpQ/3GhD5sDlnP188gjQQuweXHy9igdke5oUtVOXX1X8Sxg==
merkletreejs@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.3.9.tgz#cdb364a3b974a44f4eff3446522d7066e0cf95de"
@ -17861,12 +17875,10 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^1.0.0"
redux-localstorage-simple@^2.3.1:
version "2.4.0"
resolved "https://registry.npmjs.org/redux-localstorage-simple/-/redux-localstorage-simple-2.4.0.tgz"
integrity sha512-Zj28elJtO4fqXXC+gikonbKhFUkiwlalScYRn3EGUU44Pika1995AqUgzjIcsSPlBhIDV2WudFqa/YI9+3aE9Q==
dependencies:
merge "2.1.0"
redux-persist@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
redux-thunk@^2.4.2:
version "2.4.2"
@ -19846,6 +19858,11 @@ ts-transform-graphql-tag@^0.2.1:
resolved "https://registry.yarnpkg.com/ts-transform-graphql-tag/-/ts-transform-graphql-tag-0.2.1.tgz#f596c491196b6a6a65b65a8b99bf6e2314c78017"
integrity sha512-gciNzCpVafccayI/VQKU2ROaol4gMpz0t5sAW/jzG/J/wnjPYCn06yKzlM4mkQ5tjCvPmFuZnLYF4i0tUiIiMQ==
tsafe@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.4.tgz#048a114761714538c72f16abd25bb247d4e3780e"
integrity sha512-l4Z54QFGHO8GF0gBpb3yPGHjkIkIirl8rwW+lMBmtEMzOJeRs8BdjkDEx6nU8Ak9PQVp/KNDtECxTja8MMIDoA==
tsconfig-paths@^3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"