From 46943ad45eb58ec5819dc0d8932017144928c144 Mon Sep 17 00:00:00 2001 From: Leandro Date: Fri, 3 Nov 2023 08:03:11 -0700 Subject: [PATCH] feat(permit): load pre generated permit info (#3316) * chore: ignore nx cache folder * feat: add usePreGeneratedPermitInfo * feat: add usePreGeneratedPermitInfoForToken * feat: add usePermitCompatibleTokens * feat: check first whether token is permittable before checking locally * feat: wire token compatibility onto token widget * refactor: create a type for PermitCompatibleTokens * chore: fix cosmos * refactor: move SWR common settings into a const * chore: remove stale TODO * feat: use final url for PermitInfo on token-lists repo * refactor: simplify conditional --- .gitignore | 5 ++- .../src/modules/permit/const.ts | 3 ++ .../permit/hooks/useIsTokenPermittable.ts | 34 +++++++++++++++-- .../permit/hooks/usePermitCompatibleTokens.ts | 38 +++++++++++++++++++ .../permit/hooks/usePreGeneratedPermitInfo.ts | 29 ++++++++++++++ .../usePreGeneratedPermitInfoForToken.ts | 26 +++++++++++++ .../src/modules/permit/index.ts | 1 + .../src/modules/permit/types.ts | 2 + .../containers/SelectTokenWidget/index.tsx | 8 +++- .../pure/SelectTokenModal/index.cosmos.tsx | 1 + .../pure/SelectTokenModal/index.tsx | 6 ++- .../src/modules/tokensList/types.ts | 3 +- libs/common-const/src/common.ts | 7 ++++ 13 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts create mode 100644 apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts create mode 100644 apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfoForToken.ts diff --git a/.gitignore b/.gitignore index 936690c56a..093a092944 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,7 @@ yalc.lock # vercel .vercel -analyse.html \ No newline at end of file +analyse.html + +# NX +.nx diff --git a/apps/cowswap-frontend/src/modules/permit/const.ts b/apps/cowswap-frontend/src/modules/permit/const.ts index 877ced2cb4..757ae0aec3 100644 --- a/apps/cowswap-frontend/src/modules/permit/const.ts +++ b/apps/cowswap-frontend/src/modules/permit/const.ts @@ -9,3 +9,6 @@ export const ORDER_TYPE_SUPPORTS_PERMIT: Record = { } export const PENDING_ORDER_PERMIT_CHECK_INTERVAL = ms`1min` + +export const PRE_GENERATED_PERMIT_URL = + 'https://raw.githubusercontent.com/cowprotocol/token-lists/main/src/public/PermitInfo' diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts b/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts index 13328d0eab..834fc2af47 100644 --- a/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts +++ b/apps/cowswap-frontend/src/modules/permit/hooks/useIsTokenPermittable.ts @@ -15,6 +15,8 @@ import { TradeType } from 'modules/trade' import { useIsPermitEnabled } from 'common/hooks/featureFlags/useIsPermitEnabled' +import { usePreGeneratedPermitInfoForToken } from './usePreGeneratedPermitInfoForToken' + import { ORDER_TYPE_SUPPORTS_PERMIT } from '../const' import { addPermitInfoForTokenAtom, permittableTokensAtom } from '../state/permittableTokensAtom' import { IsTokenPermittableResult } from '../types' @@ -45,11 +47,23 @@ export function useIsTokenPermittable( const addPermitInfo = useAddPermitInfo() const permitInfo = usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined) + const { permitInfo: preGeneratedInfo, isLoading: preGeneratedIsLoading } = usePreGeneratedPermitInfoForToken(token) const spender = GP_VAULT_RELAYER[chainId] useEffect(() => { - if (!chainId || !isPermitEnabled || !lowerCaseAddress || !provider || permitInfo !== undefined || isNative) { + if ( + !chainId || + !isPermitEnabled || + !lowerCaseAddress || + !provider || + permitInfo !== undefined || + isNative || + // Do not try to load when pre-generated info is loading + preGeneratedIsLoading || + // Do not try to load when pre-generated exists + preGeneratedInfo !== undefined + ) { return } @@ -67,13 +81,25 @@ export function useIsTokenPermittable( addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: result }) } }) - }, [addPermitInfo, chainId, isNative, isPermitEnabled, lowerCaseAddress, permitInfo, provider, spender, tokenName]) + }, [ + addPermitInfo, + chainId, + isNative, + isPermitEnabled, + lowerCaseAddress, + permitInfo, + preGeneratedInfo, + preGeneratedIsLoading, + provider, + spender, + tokenName, + ]) if (isNative) { return false } - // TODO: add an updater for this - return permitInfo + + return preGeneratedInfo ?? permitInfo } /** diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts new file mode 100644 index 0000000000..4f0f3fa7d3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePermitCompatibleTokens.ts @@ -0,0 +1,38 @@ +import { useAtomValue } from 'jotai' +import { useMemo, useRef } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { usePreGeneratedPermitInfo } from './usePreGeneratedPermitInfo' + +import { permittableTokensAtom } from '../state/permittableTokensAtom' +import { PermitCompatibleTokens } from '../types' + +export function usePermitCompatibleTokens(): PermitCompatibleTokens { + const { chainId } = useWalletInfo() + const localPermitInfo = useAtomValue(permittableTokensAtom)[chainId] + const { allPermitInfo } = usePreGeneratedPermitInfo() + + const stableRef = JSON.stringify(Object.keys(localPermitInfo).concat(Object.keys(allPermitInfo))) + + const localPermitInfoRef = useRef(localPermitInfo) + localPermitInfoRef.current = localPermitInfo + const preGeneratedPermitInfoRef = useRef(allPermitInfo) + preGeneratedPermitInfoRef.current = allPermitInfo + + return useMemo(() => { + const permitCompatibleTokens: PermitCompatibleTokens = {} + + for (const address of Object.keys(preGeneratedPermitInfoRef.current)) { + permitCompatibleTokens[address.toLowerCase()] = !!preGeneratedPermitInfoRef.current[address] + } + + for (const address of Object.keys(localPermitInfoRef.current)) { + permitCompatibleTokens[address.toLowerCase()] = !!localPermitInfoRef.current[address] + } + + return permitCompatibleTokens + // Reducing unnecessary re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stableRef]) +} diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts new file mode 100644 index 0000000000..3ea379ee01 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfo.ts @@ -0,0 +1,29 @@ +import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const' +import { PermitInfo } from '@cowprotocol/permit-utils' +import { useWalletInfo } from '@cowprotocol/wallet' + +import useSWR from 'swr' + +import { PRE_GENERATED_PERMIT_URL } from '../const' + +/** + * Fetch pre-generated permit info (stored in token-lists repo) for all tokens + * + * Caches result with SWR until a page refresh + */ +export function usePreGeneratedPermitInfo(): { + allPermitInfo: Record + isLoading: boolean +} { + const { chainId } = useWalletInfo() + + const url = `${PRE_GENERATED_PERMIT_URL}.${chainId}.json` + + const { data, isLoading } = useSWR( + url, + (url: string): Promise> => fetch(url).then((r) => r.json()), + { ...SWR_NO_REFRESH_OPTIONS, fallbackData: {} } + ) + + return { allPermitInfo: data, isLoading } +} diff --git a/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfoForToken.ts b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfoForToken.ts new file mode 100644 index 0000000000..17fe2fe271 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/permit/hooks/usePreGeneratedPermitInfoForToken.ts @@ -0,0 +1,26 @@ +import { Currency } from '@uniswap/sdk-core' + +import { Nullish } from 'types' + +import { usePreGeneratedPermitInfo } from './usePreGeneratedPermitInfo' + +import { IsTokenPermittableResult } from '../types' + +/** + * Fetch pre-generated permit info for a single token + */ +export function usePreGeneratedPermitInfoForToken(token: Nullish): { + permitInfo: IsTokenPermittableResult + isLoading: boolean +} { + const { allPermitInfo, isLoading } = usePreGeneratedPermitInfo() + + const address = token && 'address' in token && token.address.toLowerCase() + + const permitInfo = address ? allPermitInfo[address] : undefined + + return { + permitInfo, + isLoading, + } +} diff --git a/apps/cowswap-frontend/src/modules/permit/index.ts b/apps/cowswap-frontend/src/modules/permit/index.ts index 9de2b7724e..c7e91b4054 100644 --- a/apps/cowswap-frontend/src/modules/permit/index.ts +++ b/apps/cowswap-frontend/src/modules/permit/index.ts @@ -2,6 +2,7 @@ export * from './hooks/useAccountAgnosticPermitHookData' export * from './hooks/useGeneratePermitHook' export * from './hooks/useIsTokenPermittable' export * from './hooks/useOrdersPermitStatus' +export * from './hooks/usePermitCompatibleTokens' export * from './types' export * from './updaters/PendingPermitUpdater' export * from './utils/handlePermit' diff --git a/apps/cowswap-frontend/src/modules/permit/types.ts b/apps/cowswap-frontend/src/modules/permit/types.ts index f3c81b4318..092236b107 100644 --- a/apps/cowswap-frontend/src/modules/permit/types.ts +++ b/apps/cowswap-frontend/src/modules/permit/types.ts @@ -48,3 +48,5 @@ export type GetPermitCacheParams = PermitCacheKeyParams export type CheckHasValidPendingPermit = (order: ParsedOrder) => Promise export type OrdersPermitStatus = Record + +export type PermitCompatibleTokens = Record diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 6bf025071d..687c0214cc 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -6,16 +6,18 @@ import { TokenWithLogo } from '@cowprotocol/common-const' import { ListState, useAddList, + useAddUserToken, useAllListsList, useAllTokens, useFavouriteTokens, - useAddUserToken, - useUserAddedTokens, useUnsupportedTokens, + useUserAddedTokens, } from '@cowprotocol/tokens' import styled from 'styled-components/macro' +import { usePermitCompatibleTokens } from 'modules/permit' + import { CowModal } from 'common/pure/Modal' import { useAllTokensBalances } from '../../hooks/useAllTokensBalances' @@ -49,6 +51,7 @@ export function SelectTokenWidget() { const allTokenLists = useAllListsList() const [balances, balancesLoading] = useAllTokensBalances() const unsupportedTokens = useUnsupportedTokens() + const permitCompatibleTokens = usePermitCompatibleTokens() const closeTokenSelectWidget = useCallback(() => { updateSelectTokenWidget({ @@ -129,6 +132,7 @@ export function SelectTokenWidget() { allTokens={allTokens} favouriteTokens={favouriteTokens} balances={balances} + permitCompatibleTokens={permitCompatibleTokens} balancesLoading={balancesLoading} onSelectToken={onSelectToken} onInputPressEnter={onInputPressEnter} diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx index 42aca59f5c..8d4b48cdc4 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.cosmos.tsx @@ -33,6 +33,7 @@ const balances = allTokensMock.reduce((acc, token) => { }, {}) const defaultProps: SelectTokenModalProps = { + permitCompatibleTokens: {}, unsupportedTokens, allTokens: allTokensMock, favouriteTokens: favouriteTokensMock, diff --git a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx index 5f2afb8125..b095e55709 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/index.tsx @@ -5,6 +5,8 @@ import { UnsupportedTokensState } from '@cowprotocol/tokens' import { Edit, X } from 'react-feather' +import { PermitCompatibleTokens } from 'modules/permit' + import * as styledEl from './styled' import { TokenAmounts } from '../../../tokens' @@ -21,6 +23,7 @@ export interface SelectTokenModalProps { unsupportedTokens: UnsupportedTokensState balancesLoading: boolean selectedToken?: string + permitCompatibleTokens: PermitCompatibleTokens onSelectToken(token: TokenWithLogo): void onInputPressEnter?(): void defaultInputValue?: string @@ -28,8 +31,6 @@ export interface SelectTokenModalProps { onDismiss(): void } -const permitCompatibleTokens: { [tokenAddress: string]: boolean } = {} // TODO: Make dynamic - export function SelectTokenModal(props: SelectTokenModalProps) { const { defaultInputValue = '', @@ -39,6 +40,7 @@ export function SelectTokenModal(props: SelectTokenModalProps) { balances, balancesLoading, unsupportedTokens, + permitCompatibleTokens, onSelectToken, onDismiss, onOpenManageWidget, diff --git a/apps/cowswap-frontend/src/modules/tokensList/types.ts b/apps/cowswap-frontend/src/modules/tokensList/types.ts index 4103353caa..fce0f34410 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/types.ts +++ b/apps/cowswap-frontend/src/modules/tokensList/types.ts @@ -1,5 +1,6 @@ import { TokenWithLogo } from '@cowprotocol/common-const' +import { PermitCompatibleTokens } from 'modules/permit' import { TokenAmounts } from 'modules/tokens' export interface SelectTokenContext { @@ -8,5 +9,5 @@ export interface SelectTokenContext { selectedToken?: string onSelectToken(token: TokenWithLogo): void unsupportedTokens: { [tokenAddress: string]: { dateAdded: number } } - permitCompatibleTokens: { [tokenAddress: string]: boolean } + permitCompatibleTokens: PermitCompatibleTokens } diff --git a/libs/common-const/src/common.ts b/libs/common-const/src/common.ts index 9a26c11884..971697adfd 100644 --- a/libs/common-const/src/common.ts +++ b/libs/common-const/src/common.ts @@ -166,6 +166,13 @@ export const SWR_OPTIONS = { revalidateOnFocus: false, } +export const SWR_NO_REFRESH_OPTIONS = { + // Cache indefinitely + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, +} + // TODO: show banner warning when PINATA env vars are missing export const COW_IPFS_OPTIONS: IpfsConfig = { pinataApiKey: PINATA_API_KEY,