Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow adding networks without invalidating local-storage #5134

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<HookDappIframe['url'], HookDappIframe>

Expand All @@ -18,7 +19,7 @@ type CustomHooksState = {

const EMPTY_STATE: CustomHooksState = { pre: {}, post: {} }

const customHookDappsInner = atomWithStorage<Record<SupportedChainId, CustomHooksState>>(
const customHookDappsInner = atomWithStorage<PersistentStateByChain<CustomHooksState>>(
'customHookDappsAtom:v1',
mapSupportedNetworks(EMPTY_STATE),
getJotaiIsolatedStorage(),
Expand All @@ -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,
},
},
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ 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[]
postHooks: CowHookDetails[]
}

type StatePerAccount = Record<string, HooksStoreState>
type StatePerNetwork = Record<SupportedChainId, StatePerAccount>
type StatePerNetwork = PersistentStateByChain<StatePerAccount>

const EMPTY_STATE: HooksStoreState = {
preHooks: [],
Expand All @@ -28,19 +29,22 @@ const hooksAtomInner = atomWithStorage<StatePerNetwork>(
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<HooksStoreState>) => {
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,
},
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, PermitInfo>

Expand All @@ -16,10 +17,10 @@ type PermittableTokens = Record<string, PermitInfo>
* Contains the permit info for every token checked locally
*/

export const permittableTokensAtom = atomWithStorage<Record<SupportedChainId, PermittableTokens>>(
export const permittableTokensAtom = atomWithStorage<PersistentStateByChain<PermittableTokens>>(
'permittableTokens:v3',
mapSupportedNetworks({}),
getJotaiMergerStorage()
getJotaiMergerStorage(),
)

/**
Expand All @@ -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)
}
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedChainId, number | null>
type SlippageBpsPerNetwork = PersistentStateByChain<number>

type SlippageType = 'smart' | 'default' | 'user'

const normalTradeSlippageAtom = atomWithStorage<SlippageBpsPerNetwork>(
'swapSlippageAtom:v0',
mapSupportedNetworks(null),
mapSupportedNetworks(undefined),
)

const ethFlowSlippageAtom = atomWithStorage<SlippageBpsPerNetwork>('ethFlowSlippageAtom:v0', mapSupportedNetworks(null))
const ethFlowSlippageAtom = atomWithStorage<SlippageBpsPerNetwork>(
'ethFlowSlippageAtom:v0',
mapSupportedNetworks(undefined),
)

export const smartTradeSlippageAtom = atom<number | null>(null)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@ 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<SupportedChainId, UnknownCurrencies>
type UnknownCurrenciesMap = PersistentStateByChain<UnknownCurrencies>

let coingeckoRateLimitHitTimestamp: null | number = null
let defillamaRateLimitHitTimestamp: null | number = null

const coingeckoUnknownCurrencies: Record<SupportedChainId, UnknownCurrencies> = mapSupportedNetworks({})
const defillamaUnknownCurrencies: Record<SupportedChainId, UnknownCurrencies> = mapSupportedNetworks({})
const coingeckoUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({})
const defillamaUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({})

function getShouldSkipCoingecko(currency: Token): boolean {
return getShouldSkipPriceSource(
currency,
COINGECKO_PLATFORMS,
coingeckoUnknownCurrencies,
coingeckoRateLimitHitTimestamp,
COINGECKO_RATE_LIMIT_TIMEOUT
COINGECKO_RATE_LIMIT_TIMEOUT,
)
}

Expand All @@ -31,7 +32,7 @@ function getShouldSkipDefillama(currency: Token): boolean {
DEFILLAMA_PLATFORMS,
defillamaUnknownCurrencies,
defillamaRateLimitHitTimestamp,
DEFILLAMA_RATE_LIMIT_TIMEOUT
DEFILLAMA_RATE_LIMIT_TIMEOUT,
)
}

Expand All @@ -40,13 +41,14 @@ function getShouldSkipPriceSource(
platforms: Record<SupportedChainId, string | null>,
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
}
Expand All @@ -58,7 +60,7 @@ function getShouldSkipPriceSource(
*/
export function fetchCurrencyUsdPrice(
currency: Token,
getUsdcPrice: () => Promise<Fraction | null>
getUsdcPrice: () => Promise<Fraction | null>,
): Promise<Fraction | null> {
const shouldSkipCoingecko = getShouldSkipCoingecko(currency)
const shouldSkipDefillama = getShouldSkipDefillama(currency)
Expand All @@ -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))
}
Expand All @@ -102,14 +104,22 @@ function handleErrorFactory(
currency: Token,
rateLimitTimestamp: null | number,
unknownCurrenciesMap: UnknownCurrenciesMap,
fetchPriceFallback: (currency: Token) => Promise<Fraction | null>
fetchPriceFallback: (currency: Token) => Promise<Fraction | null>,
): ((reason: any) => Fraction | PromiseLike<Fraction | null> | 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 {
}

Expand Down
12 changes: 6 additions & 6 deletions apps/explorer/src/api/tenderly/tenderlyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedChainId, string> = mapSupportedNetworks(
(_networkId: SupportedChainId): string => `${TENDERLY_API_URL}/${_networkId}`
const API_BASE_URLs: Record<SupportedChainId, string | undefined> = 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
}
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function getTransactionContracts(networkId: SupportedChainId, txHas

export async function getTradesAndTransfers(
networkId: SupportedChainId,
txHash: string
txHash: string,
): Promise<TxTradesAndTransfers> {
const trace = await _fetchTrace(networkId, txHash)

Expand Down Expand Up @@ -132,7 +132,7 @@ export async function getTradesAccount(
networkId: SupportedChainId,
txHash: string,
trades: Array<Trade>,
transfers: Array<Transfer>
transfers: Array<Transfer>,
): Promise<Map<string, Account>> {
const contracts = await _fetchTradesAccounts(networkId, txHash)

Expand All @@ -146,7 +146,7 @@ export async function getTradesAccount(
export function accountAddressesInvolved(
contracts: Contract[],
trades: Array<Trade>,
transfers: Array<Transfer>
transfers: Array<Transfer>,
): Map<string, Account> {
const result = new Map()

Expand Down
7 changes: 4 additions & 3 deletions apps/explorer/src/state/erc20/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedChainId, Record<string, TokenErc20>>
export type TokensLoadedFromChain = PersistentStateByChain<Record<string, TokenErc20>>

const DEFAULT_TOKENS_LOADED_FROM_CHAIN: TokensLoadedFromChain = mapSupportedNetworks({})

export const tokensLoadedFromChainAtom = atomWithStorage<TokensLoadedFromChain>(
'tokensLoadedFromChain:v0',
DEFAULT_TOKENS_LOADED_FROM_CHAIN
DEFAULT_TOKENS_LOADED_FROM_CHAIN,
)

type AddLoadedTokens = {
Expand All @@ -32,7 +33,7 @@ export const addLoadedTokensToChainAtom = atom(null, (get, set, { chainId, token
}
return acc
},
{ ...chainTokens }
{ ...chainTokens },
)

set(tokensLoadedFromChainAtom, { ...current, [chainId]: updatedChainTokens })
Expand Down
3 changes: 2 additions & 1 deletion libs/balances-and-allowances/src/state/balancesAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SupportedChainId, Record<string, string>>
type BalancesCache = PersistentStateByChain<Record<string, string>>

export interface BalancesState extends Erc20MulticallState {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function BalancesCacheUpdater({ chainId, account }: { chainId: SupportedC
{} as Record<string, string>,
)

const currentCache = state[chainId]
const currentCache = state[chainId] || {}
// Remove zero balances from the current cache
const updatedCache = Object.keys(currentCache).reduce(
(acc, tokenAddress) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) || {}
Comment on lines +6 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the recent issue returning a new array on every render, shouldn't this also have a stable empty value declared outside of the hook?

}
Loading
Loading