diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts index 9d7a07dba0..95c2842ebd 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts @@ -8,6 +8,7 @@ import { walletInfoAtom } from '@cowprotocol/wallet' import { setHooksAtom } from './hookDetailsAtom' import { HookDappIframe } from '../types/hooks' +import { PersistentStateByChain } from '@cowprotocol/types' type CustomHookDapps = Record @@ -18,7 +19,7 @@ type CustomHooksState = { const EMPTY_STATE: CustomHooksState = { pre: {}, post: {} } -const customHookDappsInner = atomWithStorage>( +const customHookDappsInner = atomWithStorage>( 'customHookDappsAtom:v1', mapSupportedNetworks(EMPTY_STATE), getJotaiIsolatedStorage(), @@ -42,13 +43,14 @@ export const customPostHookDappsAtom = atom((get) => { export const upsertCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean, dapp: HookDappIframe) => { const { chainId } = get(walletInfoAtom) const state = get(customHookDappsInner) + const stateForChain = state[chainId] || EMPTY_STATE set(customHookDappsInner, { ...state, [chainId]: { ...state[chainId], [isPreHook ? 'pre' : 'post']: { - ...state[chainId][isPreHook ? 'pre' : 'post'], + ...stateForChain[isPreHook ? 'pre' : 'post'], [dapp.url]: dapp, }, }, @@ -58,10 +60,15 @@ export const upsertCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean export const removeCustomHookDappAtom = atom(null, (get, set, dapp: HookDappIframe) => { const { chainId } = get(walletInfoAtom) const state = get(customHookDappsInner) - const currentState = { ...state[chainId] } - - delete currentState.pre[dapp.url] - delete currentState.post[dapp.url] + const stateForChain = state[chainId] || EMPTY_STATE + const currentState = { ...stateForChain } + + if (currentState.pre) { + delete currentState.pre[dapp.url] + } + if (currentState.post) { + delete currentState.post[dapp.url] + } set(customHookDappsInner, { ...state, diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts index 31c52be100..704b5f55a5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/hookDetailsAtom.ts @@ -5,6 +5,7 @@ import { getJotaiIsolatedStorage } from '@cowprotocol/core' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { walletInfoAtom } from '@cowprotocol/wallet' +import { PersistentStateByChain } from '@cowprotocol/types' export type HooksStoreState = { preHooks: CowHookDetails[] @@ -12,7 +13,7 @@ export type HooksStoreState = { } type StatePerAccount = Record -type StatePerNetwork = Record +type StatePerNetwork = PersistentStateByChain const EMPTY_STATE: HooksStoreState = { preHooks: [], @@ -28,19 +29,22 @@ const hooksAtomInner = atomWithStorage( export const hooksAtom = atom((get) => { const { chainId, account = '' } = get(walletInfoAtom) const state = get(hooksAtomInner) + const stateForChain = state[chainId] || {} - return state[chainId][account] || EMPTY_STATE + return stateForChain[account] || EMPTY_STATE }) export const setHooksAtom = atom(null, (get, set, update: SetStateAction) => { const { chainId, account = '' } = get(walletInfoAtom) set(hooksAtomInner, (state) => { + const stateForChain = state[chainId] || {} + return { ...state, [chainId]: { ...state[chainId], - [account]: typeof update === 'function' ? update(state[chainId][account] || EMPTY_STATE) : update, + [account]: typeof update === 'function' ? update(stateForChain[account] || EMPTY_STATE) : update, }, } }) diff --git a/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts b/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts index 95af95e86c..7b122f6f24 100644 --- a/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts +++ b/apps/cowswap-frontend/src/modules/permit/state/permittableTokensAtom.ts @@ -6,6 +6,7 @@ import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { PermitInfo } from '@cowprotocol/permit-utils' import { AddPermitTokenParams } from '../types' +import { PersistentStateByChain } from '@cowprotocol/types' type PermittableTokens = Record @@ -16,10 +17,10 @@ type PermittableTokens = Record * Contains the permit info for every token checked locally */ -export const permittableTokensAtom = atomWithStorage>( +export const permittableTokensAtom = atomWithStorage>( 'permittableTokens:v3', mapSupportedNetworks({}), - getJotaiMergerStorage() + getJotaiMergerStorage(), ) /** @@ -29,9 +30,13 @@ export const addPermitInfoForTokenAtom = atom( null, (get, set, { chainId, tokenAddress, permitInfo }: AddPermitTokenParams) => { const permittableTokens = { ...get(permittableTokensAtom) } + const permittableTokensForChain = permittableTokens[chainId] || {} - permittableTokens[chainId][tokenAddress.toLowerCase()] = permitInfo + permittableTokens[chainId] = { + ...permittableTokensForChain, + [tokenAddress.toLowerCase()]: permitInfo, + } set(permittableTokensAtom, permittableTokens) - } + }, ) diff --git a/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts b/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts index f1d081e351..63824e2961 100644 --- a/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeSlippage/state/slippageValueAndTypeAtom.ts @@ -7,17 +7,21 @@ import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { walletInfoAtom } from '@cowprotocol/wallet' import { isEoaEthFlowAtom } from 'modules/trade' +import { PersistentStateByChain } from '@cowprotocol/types' -type SlippageBpsPerNetwork = Record +type SlippageBpsPerNetwork = PersistentStateByChain type SlippageType = 'smart' | 'default' | 'user' const normalTradeSlippageAtom = atomWithStorage( 'swapSlippageAtom:v0', - mapSupportedNetworks(null), + mapSupportedNetworks(undefined), ) -const ethFlowSlippageAtom = atomWithStorage('ethFlowSlippageAtom:v0', mapSupportedNetworks(null)) +const ethFlowSlippageAtom = atomWithStorage( + 'ethFlowSlippageAtom:v0', + mapSupportedNetworks(undefined), +) export const smartTradeSlippageAtom = atom(null) diff --git a/apps/cowswap-frontend/src/modules/usdAmount/services/fetchCurrencyUsdPrice.ts b/apps/cowswap-frontend/src/modules/usdAmount/services/fetchCurrencyUsdPrice.ts index 8b4d9add14..92ca218dd4 100644 --- a/apps/cowswap-frontend/src/modules/usdAmount/services/fetchCurrencyUsdPrice.ts +++ b/apps/cowswap-frontend/src/modules/usdAmount/services/fetchCurrencyUsdPrice.ts @@ -5,15 +5,16 @@ import { RateLimitError, UnknownCurrencyError } from '../apis/errors' import { COINGECKO_PLATFORMS, COINGECKO_RATE_LIMIT_TIMEOUT, getCoingeckoUsdPrice } from '../apis/getCoingeckoUsdPrice' import { getCowProtocolUsdPrice } from '../apis/getCowProtocolUsdPrice' import { DEFILLAMA_PLATFORMS, DEFILLAMA_RATE_LIMIT_TIMEOUT, getDefillamaUsdPrice } from '../apis/getDefillamaUsdPrice' +import { PersistentStateByChain } from '@cowprotocol/types' type UnknownCurrencies = { [address: string]: true } -type UnknownCurrenciesMap = Record +type UnknownCurrenciesMap = PersistentStateByChain let coingeckoRateLimitHitTimestamp: null | number = null let defillamaRateLimitHitTimestamp: null | number = null -const coingeckoUnknownCurrencies: Record = mapSupportedNetworks({}) -const defillamaUnknownCurrencies: Record = mapSupportedNetworks({}) +const coingeckoUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({}) +const defillamaUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({}) function getShouldSkipCoingecko(currency: Token): boolean { return getShouldSkipPriceSource( @@ -21,7 +22,7 @@ function getShouldSkipCoingecko(currency: Token): boolean { COINGECKO_PLATFORMS, coingeckoUnknownCurrencies, coingeckoRateLimitHitTimestamp, - COINGECKO_RATE_LIMIT_TIMEOUT + COINGECKO_RATE_LIMIT_TIMEOUT, ) } @@ -31,7 +32,7 @@ function getShouldSkipDefillama(currency: Token): boolean { DEFILLAMA_PLATFORMS, defillamaUnknownCurrencies, defillamaRateLimitHitTimestamp, - DEFILLAMA_RATE_LIMIT_TIMEOUT + DEFILLAMA_RATE_LIMIT_TIMEOUT, ) } @@ -40,13 +41,14 @@ function getShouldSkipPriceSource( platforms: Record, unknownCurrenciesMap: UnknownCurrenciesMap, rateLimitTimestamp: null | number, - timeout: number + timeout: number, ): boolean { const chainId = currency.chainId as SupportedChainId + const unknownCurrenciesForChain = unknownCurrenciesMap[chainId] || {} if (!platforms[chainId]) return true - if (unknownCurrenciesMap[chainId][currency.address.toLowerCase()]) return true + if (unknownCurrenciesForChain[currency.address.toLowerCase()]) return true return !!rateLimitTimestamp && Date.now() - rateLimitTimestamp < timeout } @@ -58,7 +60,7 @@ function getShouldSkipPriceSource( */ export function fetchCurrencyUsdPrice( currency: Token, - getUsdcPrice: () => Promise + getUsdcPrice: () => Promise, ): Promise { const shouldSkipCoingecko = getShouldSkipCoingecko(currency) const shouldSkipDefillama = getShouldSkipDefillama(currency) @@ -81,19 +83,19 @@ export function fetchCurrencyUsdPrice( // No coingecko. Try Defillama, then cow if (shouldSkipCoingecko) { return getDefillamaUsdPrice(currency).catch( - handleErrorFactory(currency, defillamaRateLimitHitTimestamp, defillamaUnknownCurrencies, getCowPrice) + handleErrorFactory(currency, defillamaRateLimitHitTimestamp, defillamaUnknownCurrencies, getCowPrice), ) } // No Defillama. Try coingecko, then cow if (shouldSkipDefillama) { return getCoingeckoUsdPrice(currency).catch( - handleErrorFactory(currency, coingeckoRateLimitHitTimestamp, coingeckoUnknownCurrencies, getCowPrice) + handleErrorFactory(currency, coingeckoRateLimitHitTimestamp, coingeckoUnknownCurrencies, getCowPrice), ) } // Both coingecko and defillama available. Try coingecko, then defillama, then cow return getCoingeckoUsdPrice(currency) .catch( - handleErrorFactory(currency, coingeckoRateLimitHitTimestamp, coingeckoUnknownCurrencies, getDefillamaUsdPrice) + handleErrorFactory(currency, coingeckoRateLimitHitTimestamp, coingeckoUnknownCurrencies, getDefillamaUsdPrice), ) .catch(handleErrorFactory(currency, defillamaRateLimitHitTimestamp, defillamaUnknownCurrencies, getCowPrice)) } @@ -102,14 +104,22 @@ function handleErrorFactory( currency: Token, rateLimitTimestamp: null | number, unknownCurrenciesMap: UnknownCurrenciesMap, - fetchPriceFallback: (currency: Token) => Promise + fetchPriceFallback: (currency: Token) => Promise, ): ((reason: any) => Fraction | PromiseLike | null) | null | undefined { return (error) => { if (error instanceof RateLimitError) { rateLimitTimestamp = Date.now() } else if (error instanceof UnknownCurrencyError) { // Mark currency as unknown - unknownCurrenciesMap[currency.chainId as SupportedChainId][currency.address.toLowerCase()] = true + const chainId = currency.chainId as SupportedChainId + const unknownCurrenciesForChain = unknownCurrenciesMap[chainId] + const addressToLowercase = currency.address.toLowerCase() + + if (unknownCurrenciesForChain === undefined) { + unknownCurrenciesMap[chainId] = { [addressToLowercase]: true } + } else { + unknownCurrenciesForChain[addressToLowercase] = true + } } else { } diff --git a/apps/explorer/src/api/tenderly/tenderlyApi.ts b/apps/explorer/src/api/tenderly/tenderlyApi.ts index 726e557868..476948534d 100644 --- a/apps/explorer/src/api/tenderly/tenderlyApi.ts +++ b/apps/explorer/src/api/tenderly/tenderlyApi.ts @@ -22,15 +22,15 @@ import { SPECIAL_ADDRESSES } from '../../explorer/const' export const ALIAS_TRADER_NAME = 'Trader' const COW_PROTOCOL_CONTRACT_NAME = 'GPv2Settlement' -const API_BASE_URLs: Record = mapSupportedNetworks( - (_networkId: SupportedChainId): string => `${TENDERLY_API_URL}/${_networkId}` +const API_BASE_URLs: Record = mapSupportedNetworks( + (_networkId: SupportedChainId): string => `${TENDERLY_API_URL}/${_networkId}`, ) function _getApiBaseUrl(networkId: SupportedChainId): string { const baseUrl = API_BASE_URLs[networkId] if (!baseUrl) { - throw new Error('Unsupported Network. The tenderly API is not available in the SupportedChainId ' + networkId) + throw new Error('Unsupported Network. The tenderly API is not available or configured for chain id ' + networkId) } else { return baseUrl } @@ -65,7 +65,7 @@ export async function getTransactionContracts(networkId: SupportedChainId, txHas export async function getTradesAndTransfers( networkId: SupportedChainId, - txHash: string + txHash: string, ): Promise { const trace = await _fetchTrace(networkId, txHash) @@ -132,7 +132,7 @@ export async function getTradesAccount( networkId: SupportedChainId, txHash: string, trades: Array, - transfers: Array + transfers: Array, ): Promise> { const contracts = await _fetchTradesAccounts(networkId, txHash) @@ -146,7 +146,7 @@ export async function getTradesAccount( export function accountAddressesInvolved( contracts: Contract[], trades: Array, - transfers: Array + transfers: Array, ): Map { const result = new Map() diff --git a/apps/explorer/src/state/erc20/atoms.ts b/apps/explorer/src/state/erc20/atoms.ts index 3139787728..759fd60053 100644 --- a/apps/explorer/src/state/erc20/atoms.ts +++ b/apps/explorer/src/state/erc20/atoms.ts @@ -4,14 +4,15 @@ import { atomWithStorage } from 'jotai/utils' import { SupportedChainId, mapSupportedNetworks } from '@cowprotocol/cow-sdk' import { TokenErc20 } from '@gnosis.pm/dex-js' +import { PersistentStateByChain } from '@cowprotocol/types' -export type TokensLoadedFromChain = Record> +export type TokensLoadedFromChain = PersistentStateByChain> const DEFAULT_TOKENS_LOADED_FROM_CHAIN: TokensLoadedFromChain = mapSupportedNetworks({}) export const tokensLoadedFromChainAtom = atomWithStorage( 'tokensLoadedFromChain:v0', - DEFAULT_TOKENS_LOADED_FROM_CHAIN + DEFAULT_TOKENS_LOADED_FROM_CHAIN, ) type AddLoadedTokens = { @@ -32,7 +33,7 @@ export const addLoadedTokensToChainAtom = atom(null, (get, set, { chainId, token } return acc }, - { ...chainTokens } + { ...chainTokens }, ) set(tokensLoadedFromChainAtom, { ...current, [chainId]: updatedChainTokens }) diff --git a/libs/balances-and-allowances/src/state/balancesAtom.ts b/libs/balances-and-allowances/src/state/balancesAtom.ts index a9f3f7c3fc..a92d1b32ec 100644 --- a/libs/balances-and-allowances/src/state/balancesAtom.ts +++ b/libs/balances-and-allowances/src/state/balancesAtom.ts @@ -4,8 +4,9 @@ import { getJotaiMergerStorage } from '@cowprotocol/core' import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { Erc20MulticallState } from '../types' +import { PersistentStateByChain } from '@cowprotocol/types' -type BalancesCache = Record> +type BalancesCache = PersistentStateByChain> export interface BalancesState extends Erc20MulticallState {} diff --git a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx index 5ad2d697df..7c0eab6b80 100644 --- a/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx +++ b/libs/balances-and-allowances/src/updaters/BalancesCacheUpdater.tsx @@ -34,7 +34,7 @@ export function BalancesCacheUpdater({ chainId, account }: { chainId: SupportedC {} as Record, ) - const currentCache = state[chainId] + const currentCache = state[chainId] || {} // Remove zero balances from the current cache const updatedCache = Object.keys(currentCache).reduce( (acc, tokenAddress) => { diff --git a/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts b/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts index 851ff6619e..9452713858 100644 --- a/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts +++ b/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts @@ -1,7 +1,8 @@ import { useAtomValue } from 'jotai' import { currentUnsupportedTokensAtom } from '../../../state/tokens/unsupportedTokensAtom' +import { UnsupportedTokensState } from '@cowprotocol/tokens' -export function useUnsupportedTokens() { - return useAtomValue(currentUnsupportedTokensAtom) +export function useUnsupportedTokens(): UnsupportedTokensState { + return useAtomValue(currentUnsupportedTokensAtom) || {} } diff --git a/libs/tokens/src/state/tokenLists/tokenListsActionsAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsActionsAtom.ts index 086116dcb5..3782d4ec1a 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsActionsAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsActionsAtom.ts @@ -36,6 +36,7 @@ export const upsertListsAtom = atom(null, (get, set, chainId: SupportedChainId, export const addListAtom = atom(null, (get, set, state: ListState) => { const { chainId, widgetAppCode } = get(environmentAtom) const userAddedTokenLists = get(userAddedListsSourcesAtom) + const userAddedTokenListsForChain = userAddedTokenLists[chainId] || [] state.isEnabled = true @@ -45,7 +46,7 @@ export const addListAtom = atom(null, (get, set, state: ListState) => { set(userAddedListsSourcesAtom, { ...userAddedTokenLists, - [chainId]: userAddedTokenLists[chainId].concat({ + [chainId]: userAddedTokenListsForChain.concat({ widgetAppCode: state.widgetAppCode, priority: state.priority, source: state.source, @@ -58,10 +59,11 @@ export const addListAtom = atom(null, (get, set, state: ListState) => { export const removeListAtom = atom(null, (get, set, source: string) => { const { chainId } = get(environmentAtom) const userAddedTokenLists = get(userAddedListsSourcesAtom) + const userAddedTokenListsForChain = userAddedTokenLists[chainId] || [] set(userAddedListsSourcesAtom, { ...userAddedTokenLists, - [chainId]: userAddedTokenLists[chainId].filter((item) => item.source !== source), + [chainId]: userAddedTokenListsForChain.filter((item) => item.source !== source), }) const stateCopy = { ...get(listsStatesByChainAtom) } diff --git a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts index 77bf475d43..5db1f5bbc6 100644 --- a/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts +++ b/libs/tokens/src/state/tokenLists/tokenListsStateAtom.ts @@ -46,14 +46,15 @@ export const userAddedListsSourcesAtom = atomWithStorage( export const allListsSourcesAtom = atom((get) => { const { chainId, useCuratedListOnly, isYieldEnabled } = get(environmentAtom) const userAddedTokenLists = get(userAddedListsSourcesAtom) + const userAddedTokenListsForChain = userAddedTokenLists[chainId] || [] const lpLists = isYieldEnabled ? LP_TOKEN_LISTS : [] if (useCuratedListOnly) { - return [get(curatedListSourceAtom), ...lpLists, ...userAddedTokenLists[chainId]] + return [get(curatedListSourceAtom), ...lpLists, ...userAddedTokenListsForChain] } - return [...DEFAULT_TOKENS_LISTS[chainId], ...lpLists, ...(userAddedTokenLists[chainId] || [])] + return [...(DEFAULT_TOKENS_LISTS[chainId] || []), ...lpLists, ...userAddedTokenListsForChain] }) // Lists states @@ -78,13 +79,14 @@ export const listsStatesMapAtom = atom((get) => { const allTokenListsInfo = get(listsStatesByChainAtom) const virtualListsState = get(virtualListsStateAtom) const userAddedTokenLists = get(userAddedListsSourcesAtom) + const useeAddedTokenListsForChain = userAddedTokenLists[chainId] || [] const currentNetworkLists = { ...allTokenListsInfo[chainId], ...virtualListsState, } - const userAddedListSources = userAddedTokenLists[chainId].reduce<{ [key: string]: boolean }>((acc, list) => { + const userAddedListSources = useeAddedTokenListsForChain.reduce<{ [key: string]: boolean }>((acc, list) => { acc[list.source] = true return acc }, {}) diff --git a/libs/tokens/src/state/tokens/allTokensAtom.ts b/libs/tokens/src/state/tokens/allTokensAtom.ts index 2254986fd2..9225da2684 100644 --- a/libs/tokens/src/state/tokens/allTokensAtom.ts +++ b/libs/tokens/src/state/tokens/allTokensAtom.ts @@ -79,7 +79,7 @@ export const activeTokensAtom = atom((get) => { { [nativeToken.address.toLowerCase()]: nativeToken as TokenInfo, ...tokensMap.activeTokens, - ...lowerCaseTokensMap(userAddedTokens[chainId]), + ...lowerCaseTokensMap(userAddedTokens[chainId] || {}), ...lowerCaseTokensMap(favoriteTokensState[chainId]), ...(enableLpTokensByDefault ? Object.keys(tokensMap.inactiveTokens).reduce((acc, key) => { diff --git a/libs/tokens/src/state/tokens/unsupportedTokensAtom.ts b/libs/tokens/src/state/tokens/unsupportedTokensAtom.ts index 002bdd8e7f..5f6ec77259 100644 --- a/libs/tokens/src/state/tokens/unsupportedTokensAtom.ts +++ b/libs/tokens/src/state/tokens/unsupportedTokensAtom.ts @@ -6,11 +6,12 @@ import { mapSupportedNetworks, SupportedChainId } from '@cowprotocol/cow-sdk' import { UnsupportedTokensState } from '../../types' import { environmentAtom } from '../environmentAtom' +import { PersistentStateByChain } from '@cowprotocol/types' -export const unsupportedTokensAtom = atomWithStorage>( +export const unsupportedTokensAtom = atomWithStorage>( 'unsupportedTokensAtom:v2', mapSupportedNetworks({}), - getJotaiMergerStorage() + getJotaiMergerStorage(), ) export const currentUnsupportedTokensAtom = atom((get) => { @@ -22,8 +23,9 @@ export const currentUnsupportedTokensAtom = atom((get) => { export const addUnsupportedTokenAtom = atom(null, (get, set, chainId: SupportedChainId, tokenAddress: string) => { const tokenId = tokenAddress.toLowerCase() const tokenList = get(unsupportedTokensAtom) + const tokenListForChain = tokenList[chainId] || {} - if (!tokenList[chainId][tokenId]) { + if (!tokenListForChain[tokenId]) { const update: UnsupportedTokensState = { ...tokenList[chainId], [tokenId]: { dateAdded: Date.now() }, @@ -39,11 +41,12 @@ export const addUnsupportedTokenAtom = atom(null, (get, set, chainId: SupportedC export const removeUnsupportedTokensAtom = atom(null, (get, set, tokenAddresses: Array) => { const { chainId } = get(environmentAtom) const tokenList = { ...get(unsupportedTokensAtom) } + const tokenListForChain = tokenList[chainId] || {} tokenAddresses.forEach((tokenAddress) => { const tokenId = tokenAddress.toLowerCase() - delete tokenList[chainId][tokenId] + delete tokenListForChain[tokenId] }) set(unsupportedTokensAtom, tokenList) diff --git a/libs/tokens/src/state/tokens/userAddedTokensAtom.ts b/libs/tokens/src/state/tokens/userAddedTokensAtom.ts index 6b6a446223..15b184fac6 100644 --- a/libs/tokens/src/state/tokens/userAddedTokensAtom.ts +++ b/libs/tokens/src/state/tokens/userAddedTokensAtom.ts @@ -8,18 +8,20 @@ import { Token } from '@uniswap/sdk-core' import { TokensMap } from '../../types' import { environmentAtom } from '../environmentAtom' +import { PersistentStateByChain } from '@cowprotocol/types' -export const userAddedTokensAtom = atomWithStorage>( +export const userAddedTokensAtom = atomWithStorage>( 'userAddedTokensAtom:v1', mapSupportedNetworks({}), - getJotaiMergerStorage() + getJotaiMergerStorage(), ) export const userAddedTokensListAtom = atom((get) => { const { chainId } = get(environmentAtom) const userAddedTokensState = get(userAddedTokensAtom) + const userAddedTokenStateForChain = userAddedTokensState[chainId] || {} - return Object.values(userAddedTokensState[chainId]).map((token) => TokenWithLogo.fromToken(token, token.logoURI)) + return Object.values(userAddedTokenStateForChain).map((token) => TokenWithLogo.fromToken(token, token.logoURI)) }) export const addUserTokenAtom = atom(null, (get, set, tokens: TokenWithLogo[]) => { diff --git a/libs/tokens/src/types.ts b/libs/tokens/src/types.ts index 1fa4982119..c0ae2628f4 100644 --- a/libs/tokens/src/types.ts +++ b/libs/tokens/src/types.ts @@ -1,5 +1,5 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { LpTokenProvider, TokenInfo } from '@cowprotocol/types' +import { LpTokenProvider, PersistentStateByChain, TokenInfo } from '@cowprotocol/types' import type { TokenList as UniTokenList } from '@uniswap/token-lists' export enum TokenListCategory { @@ -19,7 +19,7 @@ export type ListSourceConfig = { source: string } -export type ListsSourcesByNetwork = Record> +export type ListsSourcesByNetwork = PersistentStateByChain> export type TokensMap = { [address: string]: TokenInfo } @@ -34,4 +34,4 @@ export interface ListState extends Pick +export type TokenListsByChainState = PersistentStateByChain diff --git a/libs/tokens/src/updaters/TokensListsUpdater/index.ts b/libs/tokens/src/updaters/TokensListsUpdater/index.ts index 08eae3540d..7e8294bf7b 100644 --- a/libs/tokens/src/updaters/TokensListsUpdater/index.ts +++ b/libs/tokens/src/updaters/TokensListsUpdater/index.ts @@ -16,11 +16,14 @@ import { environmentAtom, updateEnvironmentAtom } from '../../state/environmentA import { upsertListsAtom } from '../../state/tokenLists/tokenListsActionsAtom' import { allListsSourcesAtom, tokenListsUpdatingAtom } from '../../state/tokenLists/tokenListsStateAtom' import { ListState } from '../../types' +import { PersistentStateByChain } from '@cowprotocol/types' + +const LAST_UPDATE_TIME_DEFAULT = 0 const { atom: lastUpdateTimeAtom, updateAtom: updateLastUpdateTimeAtom } = atomWithPartialUpdate( - atomWithStorage>( + atomWithStorage>( 'tokens:lastUpdateTimeAtom:v4', - mapSupportedNetworks(0), + mapSupportedNetworks(LAST_UPDATE_TIME_DEFAULT), getJotaiMergerStorage(), ), ) @@ -70,7 +73,7 @@ export function TokensListsUpdater({ const { data: listsStates, isLoading } = useSWR( ['TokensListsUpdater', allTokensLists, chainId, lastUpdateTimeState], () => { - if (!getIsTimeToUpdate(lastUpdateTimeState[chainId])) return null + if (!getIsTimeToUpdate(lastUpdateTimeState[chainId] || LAST_UPDATE_TIME_DEFAULT)) return null return Promise.allSettled(allTokensLists.map(fetchTokenList)).then(getFulfilledResults) }, diff --git a/libs/types/src/common.ts b/libs/types/src/common.ts index dfb4987fa5..7537357403 100644 --- a/libs/types/src/common.ts +++ b/libs/types/src/common.ts @@ -1,3 +1,5 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' + export type Command = () => void export type StatefulValue = [T, (value: T) => void] @@ -35,3 +37,15 @@ export enum LpTokenProvider { SUSHI = 'SUSHI', PANCAKE = 'PANCAKE', } + +/** + * This helper type allows to define a state that is persisted by chain. + * + * For a lot of constants in the project we use Record to model them, so when we add new chains, we will get a compile time error until we update the new value for the added chain. + * This patter is fine for this configuration constants. + * + * However, we can't use the same pattern for modeling persisted state (in local storage for example). + * The reason is that when a user recovers a persisted value from an older version where a chain didn't exist, it will return `undefined` when we try to access the value for the new chain. + * The type won't be correct, and typescript will make us assume that the value is always defined leading to hard to debug runtime errors. + */ +export type PersistentStateByChain = Record