Commit 102a935f authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

fix: Handle new and unsupported chains passed in from GQL BE (#6878)

* add subset chain type and checks

* add sentry logging for invalid chain errors

* add test cases

* dynamic token balances

* add test for BE adding a new chain

* rename and use slice

* address comments

* undo yarn.lock changes

* make copy in utils

* chore: Declare GQL variables as readonly (#6889)

* declare gql variables as readonly

* remove console log

* Merge branch 'cab/error_supported_chain' into tina/gql-readonly

---------
Co-authored-by: default avatarCharlie B <charles@bachmeier.io>

---------
Co-authored-by: default avatarCharlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: default avatarTina <59578595+tinaszheng@users.noreply.github.com>
parent 614c1524
...@@ -15,6 +15,7 @@ const config: CodegenConfig = { ...@@ -15,6 +15,7 @@ const config: CodegenConfig = {
withHooks: true, withHooks: true,
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null // This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
maybeValue: 'T', maybeValue: 'T',
immutableTypes: true,
}, },
}, },
}, },
......
...@@ -12,6 +12,7 @@ import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart' ...@@ -12,6 +12,7 @@ import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection' import { getConnection } from 'connection'
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks' import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks' import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable' import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
...@@ -227,9 +228,10 @@ export default function AuthenticatedHeader({ account, openSettings }: { account ...@@ -227,9 +228,10 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow]) const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
const { data: portfolioBalances } = usePortfolioBalancesQuery({ const { data: portfolioBalances } = usePortfolioBalancesQuery({
variables: { ownerAddress: account ?? '' }, variables: { ownerAddress: account ?? '', chains: GQL_MAINNET_CHAINS },
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
}) })
const portfolio = portfolioBalances?.portfolios?.[0] const portfolio = portfolioBalances?.portfolios?.[0]
const totalBalance = portfolio?.tokensTotalDenominatedValue?.value const totalBalance = portfolio?.tokensTotalDenominatedValue?.value
const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value
......
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
TokenApprovalPartsFragment, TokenApprovalPartsFragment,
TokenTransferPartsFragment, TokenTransferPartsFragment,
} from 'graphql/data/__generated__/types-and-hooks' } from 'graphql/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'graphql/data/util' import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { isAddress } from 'utils' import { isAddress } from 'utils'
...@@ -76,10 +76,9 @@ function isSameAddress(a?: string, b?: string) { ...@@ -76,10 +76,9 @@ function isSameAddress(a?: string, b?: string) {
} }
function callsPositionManagerContract(assetActivity: AssetActivityPartsFragment) { function callsPositionManagerContract(assetActivity: AssetActivityPartsFragment) {
return isSameAddress( const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
assetActivity.transaction.to, if (!supportedChain) return false
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[fromGraphQLChain(assetActivity.chain)] return isSameAddress(assetActivity.transaction.to, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[supportedChain])
)
} }
// Gets counts for number of NFTs in each collection present // Gets counts for number of NFTs in each collection present
...@@ -93,15 +92,24 @@ function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key: ...@@ -93,15 +92,24 @@ function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key:
}, {} as { [key: string]: number | undefined }) }, {} as { [key: string]: number | undefined })
} }
function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment) { function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment): string | undefined {
const supportedSentChain = supportedChainIdFromGQLChain(sent.asset.chain)
const supportedReceivedChain = supportedChainIdFromGQLChain(received.asset.chain)
if (!supportedSentChain || !supportedReceivedChain) {
logSentryErrorForUnsupportedChain({
extras: { sentAsset: sent.asset, receivedAsset: received.asset },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
if ( if (
sent.tokenStandard === 'NATIVE' && sent.tokenStandard === 'NATIVE' &&
isSameAddress(nativeOnChain(fromGraphQLChain(sent.asset.chain)).wrapped.address, received.asset.address) isSameAddress(nativeOnChain(supportedSentChain).wrapped.address, received.asset.address)
) )
return t`Wrapped` return t`Wrapped`
else if ( else if (
received.tokenStandard === 'NATIVE' && received.tokenStandard === 'NATIVE' &&
isSameAddress(nativeOnChain(fromGraphQLChain(received.asset.chain)).wrapped.address, received.asset.address) isSameAddress(nativeOnChain(supportedReceivedChain).wrapped.address, received.asset.address)
) { ) {
return t`Unwrapped` return t`Unwrapped`
} else { } else {
...@@ -269,9 +277,17 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit ...@@ -269,9 +277,17 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
}, },
{ NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] } { NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] }
) )
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
if (!supportedChain) {
logSentryErrorForUnsupportedChain({
extras: { assetActivity },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
const defaultFields = { const defaultFields = {
hash: assetActivity.transaction.hash, hash: assetActivity.transaction.hash,
chainId: fromGraphQLChain(assetActivity.chain), chainId: supportedChain,
status: assetActivity.transaction.status, status: assetActivity.transaction.status,
timestamp: assetActivity.timestamp, timestamp: assetActivity.timestamp,
logos: getLogoSrcs(changes), logos: getLogoSrcs(changes),
...@@ -289,7 +305,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit ...@@ -289,7 +305,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
} }
} }
export function parseRemoteActivities(assetActivities?: AssetActivityPartsFragment[]) { export function parseRemoteActivities(assetActivities?: readonly AssetActivityPartsFragment[]) {
return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => { return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => {
const activity = parseRemoteActivity(assetActivity) const activity = parseRemoteActivity(assetActivity)
if (activity) acc[activity.hash] = activity if (activity) acc[activity.hash] = activity
......
...@@ -4,7 +4,12 @@ import { formatNumber, NumberType } from '@uniswap/conedison/format' ...@@ -4,7 +4,12 @@ import { formatNumber, NumberType } from '@uniswap/conedison/format'
import Row from 'components/Row' import Row from 'components/Row'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart' import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import { PortfolioBalancesQuery, usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks' import { PortfolioBalancesQuery, usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency } from 'graphql/data/util' import {
getTokenDetailsURL,
GQL_MAINNET_CHAINS,
gqlToCurrency,
logSentryErrorForUnsupportedChain,
} from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
...@@ -31,7 +36,7 @@ export default function Tokens({ account }: { account: string }) { ...@@ -31,7 +36,7 @@ export default function Tokens({ account }: { account: string }) {
const [showHiddenTokens, setShowHiddenTokens] = useState(false) const [showHiddenTokens, setShowHiddenTokens] = useState(false)
const { data } = usePortfolioBalancesQuery({ const { data } = usePortfolioBalancesQuery({
variables: { ownerAddress: account }, variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS },
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
errorPolicy: 'all', errorPolicy: 'all',
}) })
...@@ -103,6 +108,13 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok ...@@ -103,6 +108,13 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
}, [navigate, token, toggleWalletDrawer]) }, [navigate, token, toggleWalletDrawer])
const currency = gqlToCurrency(token) const currency = gqlToCurrency(token)
if (!currency) {
logSentryErrorForUnsupportedChain({
extras: { token },
errorMessage: 'Token from unsupported chain received from Mini Portfolio Token Balance Query',
})
return null
}
return ( return (
<TraceEvent <TraceEvent
events={[BrowserEvent.onClick]} events={[BrowserEvent.onClick]}
......
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { usePortfolioBalancesLazyQuery } from 'graphql/data/__generated__/types-and-hooks' import { usePortfolioBalancesLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react' import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'
import { useAllTransactions } from 'state/transactions/hooks' import { useAllTransactions } from 'state/transactions/hooks'
...@@ -44,7 +45,7 @@ export default function PrefetchBalancesWrapper({ children }: PropsWithChildren) ...@@ -44,7 +45,7 @@ export default function PrefetchBalancesWrapper({ children }: PropsWithChildren)
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true) const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true)
const fetchBalances = useCallback(() => { const fetchBalances = useCallback(() => {
if (account) { if (account) {
prefetchPortfolioBalances({ variables: { ownerAddress: account } }) prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
setHasUnfetchedBalances(false) setHasUnfetchedBalances(false)
} }
}, [account, prefetchPortfolioBalances]) }, [account, prefetchPortfolioBalances])
......
...@@ -3,7 +3,7 @@ import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks' ...@@ -3,7 +3,7 @@ import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens' import { SearchToken } from 'graphql/data/SearchTokens'
import { TokenQueryData } from 'graphql/data/Token' import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens' import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util' import { supportedChainIdFromGQLChain } from 'graphql/data/util'
import AssetLogo, { AssetLogoBaseProps } from './AssetLogo' import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
...@@ -12,7 +12,7 @@ export default function QueryTokenLogo( ...@@ -12,7 +12,7 @@ export default function QueryTokenLogo(
token?: TopToken | TokenQueryData | SearchToken token?: TopToken | TokenQueryData | SearchToken
} }
) { ) {
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined const chainId = props.token?.chain ? supportedChainIdFromGQLChain(props.token?.chain) : undefined
return ( return (
<AssetLogo <AssetLogo
......
import { SupportedChainId } from 'constants/chains'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks' import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens' import { SearchToken } from 'graphql/data/SearchTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util' import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { atomWithStorage, useAtomValue } from 'jotai/utils' import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { GenieCollection } from 'nft/types' import { GenieCollection } from 'nft/types'
...@@ -86,7 +85,15 @@ export function useRecentlySearchedAssets() { ...@@ -86,7 +85,15 @@ export function useRecentlySearchedAssets() {
shortenedHistory.forEach((asset) => { shortenedHistory.forEach((asset) => {
if (asset.address === 'NATIVE') { if (asset.address === 'NATIVE') {
// Handles special case where wMATIC data needs to be used for MATIC // Handles special case where wMATIC data needs to be used for MATIC
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET) const chain = supportedChainIdFromGQLChain(asset.chain)
if (!chain) {
logSentryErrorForUnsupportedChain({
extras: { asset },
errorMessage: 'Invalid chain retrieved from Seach Token/Collection Query',
})
return
}
const native = nativeOnChain(chain)
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}` const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
const result = resultsMap[queryAddress] const result = resultsMap[queryAddress]
if (result) data.push({ ...result, address: 'NATIVE', ...native }) if (result) data.push({ ...result, address: 'NATIVE', ...native })
......
...@@ -26,7 +26,7 @@ import { checkWarning } from 'constants/tokenSafety' ...@@ -26,7 +26,7 @@ import { checkWarning } from 'constants/tokenSafety'
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks' import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token' import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
import { QueryToken } from 'graphql/data/Token' import { QueryToken } from 'graphql/data/Token'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util' import { getTokenDetailsURL, InterfaceGqlChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency' import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
import { Swap } from 'pages/Swap' import { Swap } from 'pages/Swap'
...@@ -89,7 +89,7 @@ function useRelevantToken( ...@@ -89,7 +89,7 @@ function useRelevantToken(
type TokenDetailsProps = { type TokenDetailsProps = {
urlAddress?: string urlAddress?: string
inputTokenAddress?: string inputTokenAddress?: string
chain: Chain chain: InterfaceGqlChain
tokenQuery: TokenQuery tokenQuery: TokenQuery
tokenPriceQuery?: TokenPriceQuery tokenPriceQuery?: TokenPriceQuery
onChangeTimePeriod: OnChangeTimePeriod onChangeTimePeriod: OnChangeTimePeriod
...@@ -111,8 +111,7 @@ export default function TokenDetails({ ...@@ -111,8 +111,7 @@ export default function TokenDetails({
) )
const { chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain] const pageChainId = supportedChainIdFromGQLChain(chain)
const tokenQueryData = tokenQuery.token const tokenQueryData = tokenQuery.token
const crossChainMap = useMemo( const crossChainMap = useMemo(
() => () =>
...@@ -185,6 +184,7 @@ export default function TokenDetails({ ...@@ -185,6 +184,7 @@ export default function TokenDetails({
}, },
[continueSwap, setContinueSwap] [continueSwap, setContinueSwap]
) )
// address will never be undefined if token is defined; address is checked here to appease typechecker // address will never be undefined if token is defined; address is checked here to appease typechecker
if (detailedToken === undefined || !address) { if (detailedToken === undefined || !address) {
return <InvalidTokenDetails pageChainId={pageChainId} isInvalidAddress={!address} /> return <InvalidTokenDetails pageChainId={pageChainId} isInvalidAddress={!address} />
......
import Badge from 'components/Badge' import Badge from 'components/Badge'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { BACKEND_CHAIN_NAMES, CHAIN_NAME_TO_CHAIN_ID, validateUrlChainParam } from 'graphql/data/util' import { Chain } from 'graphql/data/__generated__/types-and-hooks'
import { BACKEND_CHAIN_NAMES, supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef } from 'react' import { useRef } from 'react'
import { Check, ChevronDown, ChevronUp } from 'react-feather' import { Check, ChevronDown, ChevronUp } from 'react-feather'
...@@ -116,8 +117,8 @@ export default function NetworkFilter() { ...@@ -116,8 +117,8 @@ export default function NetworkFilter() {
const { chainName } = useParams<{ chainName?: string }>() const { chainName } = useParams<{ chainName?: string }>()
const currentChainName = validateUrlChainParam(chainName) const currentChainName = validateUrlChainParam(chainName)
const chainInfo = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[currentChainName]) const chainInfo = getChainInfo(supportedChainIdFromGQLChain(currentChainName))
const BNBChainInfo = getChainInfo(CHAIN_NAME_TO_CHAIN_ID.BNB) const BNBChainInfo = getChainInfo(supportedChainIdFromGQLChain(Chain.Bnb))
return ( return (
<StyledMenu ref={node}> <StyledMenu ref={node}>
...@@ -129,7 +130,7 @@ export default function NetworkFilter() { ...@@ -129,7 +130,7 @@ export default function NetworkFilter() {
> >
<StyledMenuContent> <StyledMenuContent>
<NetworkLabel> <NetworkLabel>
<Logo src={chainInfo?.logoUrl} /> {chainInfo?.label} <Logo src={chainInfo.logoUrl} /> {chainInfo.label}
</NetworkLabel> </NetworkLabel>
<Chevron open={open}> <Chevron open={open}>
{open ? ( {open ? (
...@@ -143,8 +144,7 @@ export default function NetworkFilter() { ...@@ -143,8 +144,7 @@ export default function NetworkFilter() {
{open && ( {open && (
<MenuTimeFlyout> <MenuTimeFlyout>
{BACKEND_CHAIN_NAMES.map((network) => { {BACKEND_CHAIN_NAMES.map((network) => {
const chainInfo = getChainInfo(CHAIN_NAME_TO_CHAIN_ID[network]) const chainInfo = getChainInfo(supportedChainIdFromGQLChain(network))
if (!chainInfo) return null
return ( return (
<InternalLinkMenuItem <InternalLinkMenuItem
key={network} key={network}
......
...@@ -7,7 +7,7 @@ import SparklineChart from 'components/Charts/SparklineChart' ...@@ -7,7 +7,7 @@ import SparklineChart from 'components/Charts/SparklineChart'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo' import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens' import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL, validateUrlChainParam } from 'graphql/data/util' import { getTokenDetailsURL, supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ForwardedRef, forwardRef } from 'react' import { ForwardedRef, forwardRef } from 'react'
import { CSSProperties, ReactNode } from 'react' import { CSSProperties, ReactNode } from 'react'
...@@ -435,7 +435,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -435,7 +435,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const filterNetwork = validateUrlChainParam(useParams<{ chainName?: string }>().chainName?.toUpperCase()) const filterNetwork = validateUrlChainParam(useParams<{ chainName?: string }>().chainName?.toUpperCase())
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork] const chainId = supportedChainIdFromGQLChain(filterNetwork)
const timePeriod = useAtomValue(filterTimeAtom) const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value const delta = token.market?.pricePercentChange?.value
const arrow = getDeltaArrow(delta) const arrow = getDeltaArrow(delta)
......
...@@ -3,7 +3,7 @@ import gql from 'graphql-tag' ...@@ -3,7 +3,7 @@ import gql from 'graphql-tag'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { TokenQuery } from './__generated__/types-and-hooks' import { TokenQuery } from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID } from './util' import { supportedChainIdFromGQLChain } from './util'
// The difference between Token and TokenProject: // The difference between Token and TokenProject:
// Token: an on-chain entity referring to a contract (e.g. uni token on ethereum 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984) // Token: an on-chain entity referring to a contract (e.g. uni token on ethereum 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984)
...@@ -70,8 +70,10 @@ export type TokenQueryData = TokenQuery['token'] ...@@ -70,8 +70,10 @@ export type TokenQueryData = TokenQuery['token']
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces. // TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo { export class QueryToken extends WrappedTokenInfo {
constructor(address: string, data: NonNullable<TokenQueryData>, logoSrc?: string) { constructor(address: string, data: NonNullable<TokenQueryData>, logoSrc?: string) {
const chainId = supportedChainIdFromGQLChain(data.chain)
if (chainId) {
super({ super({
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain], chainId,
address, address,
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS, decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
symbol: data.symbol ?? '', symbol: data.symbol ?? '',
...@@ -79,4 +81,5 @@ export class QueryToken extends WrappedTokenInfo { ...@@ -79,4 +81,5 @@ export class QueryToken extends WrappedTokenInfo {
logoURI: logoSrc ?? data.project?.logoUrl ?? undefined, logoURI: logoSrc ?? data.project?.logoUrl ?? undefined,
}) })
} }
}
} }
...@@ -16,10 +16,10 @@ import { ...@@ -16,10 +16,10 @@ import {
useTopTokensSparklineQuery, useTopTokensSparklineQuery,
} from './__generated__/types-and-hooks' } from './__generated__/types-and-hooks'
import { import {
CHAIN_NAME_TO_CHAIN_ID,
isPricePoint, isPricePoint,
PollingInterval, PollingInterval,
PricePoint, PricePoint,
supportedChainIdFromGQLChain,
toHistoryDuration, toHistoryDuration,
unwrapToken, unwrapToken,
usePollQueryWhileMounted, usePollQueryWhileMounted,
...@@ -140,14 +140,14 @@ export type SparklineMap = { [key: string]: PricePoint[] | undefined } ...@@ -140,14 +140,14 @@ export type SparklineMap = { [key: string]: PricePoint[] | undefined }
export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number] export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number]
interface UseTopTokensReturnValue { interface UseTopTokensReturnValue {
tokens?: TopToken[] tokens?: readonly TopToken[]
tokenSortRank: Record<string, number> tokenSortRank: Record<string, number>
loadingTokens: boolean loadingTokens: boolean
sparklines: SparklineMap sparklines: SparklineMap
} }
export function useTopTokens(chain: Chain): UseTopTokensReturnValue { export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain] const chainId = supportedChainIdFromGQLChain(chain)
const duration = toHistoryDuration(useAtomValue(filterTimeAtom)) const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const { data: sparklineQuery } = usePollQueryWhileMounted( const { data: sparklineQuery } = usePollQueryWhileMounted(
...@@ -158,7 +158,7 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue { ...@@ -158,7 +158,7 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
) )
const sparklines = useMemo(() => { const sparklines = useMemo(() => {
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken)) const unwrappedTokens = chainId && sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
const map: SparklineMap = {} const map: SparklineMap = {}
unwrappedTokens?.forEach( unwrappedTokens?.forEach(
(current) => current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint)) (current) => current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint))
...@@ -173,7 +173,10 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue { ...@@ -173,7 +173,10 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
PollingInterval.Fast PollingInterval.Fast
) )
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data]) const unwrappedTokens = useMemo(
() => chainId && data?.topTokens?.map((token) => unwrapToken(chainId, token)),
[chainId, data]
)
const sortedTokens = useSortedTokens(unwrappedTokens) const sortedTokens = useSortedTokens(unwrappedTokens)
const tokenSortRank = useMemo( const tokenSortRank = useMemo(
() => () =>
......
import gql from 'graphql-tag' import gql from 'graphql-tag'
gql` gql`
query PortfolioBalances($ownerAddress: String!) { query PortfolioBalances($ownerAddress: String!, $chains: [Chain!]!) {
portfolios(ownerAddresses: [$ownerAddress], chains: [ETHEREUM, POLYGON, ARBITRUM, OPTIMISM, BNB]) { portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
id id
tokensTotalDenominatedValue { tokensTotalDenominatedValue {
id id
......
import { SupportedChainId } from 'constants/chains'
import { Chain } from './__generated__/types-and-hooks'
import { isSupportedGQLChain, supportedChainIdFromGQLChain } from './util'
describe('fromGraphQLChain', () => {
it('should return the corresponding chain ID for supported chains', () => {
expect(supportedChainIdFromGQLChain(Chain.Ethereum)).toBe(SupportedChainId.MAINNET)
for (const chain of Object.values(Chain)) {
if (!isSupportedGQLChain(chain)) continue
expect(supportedChainIdFromGQLChain(chain)).not.toBe(undefined)
}
})
it('should return undefined for unsupported chains', () => {
expect(supportedChainIdFromGQLChain(Chain.UnknownChain)).toBe(undefined)
for (const chain of Object.values(Chain)) {
if (isSupportedGQLChain(chain)) continue
expect(supportedChainIdFromGQLChain(chain)).toBe(undefined)
}
})
it('should not crash when a new BE chain is added', () => {
enum NewChain {
NewChain = 'NEW_CHAIN',
}
const ExpandedChainList = [...Object.values(Chain), NewChain.NewChain as unknown as Chain]
for (const chain of ExpandedChainList) {
if (isSupportedGQLChain(chain)) continue
expect(supportedChainIdFromGQLChain(chain)).toBe(undefined)
}
})
})
import { QueryResult } from '@apollo/client' import { QueryResult } from '@apollo/client'
import * as Sentry from '@sentry/react'
import { Currency, Token } from '@uniswap/sdk-core' import { Currency, Token } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
...@@ -56,10 +57,24 @@ export function isPricePoint(p: PricePoint | null): p is PricePoint { ...@@ -56,10 +57,24 @@ export function isPricePoint(p: PricePoint | null): p is PricePoint {
return p !== null return p !== null
} }
// TODO(DAT-33) Update when BE adds Ethereum Sepolia to supported chains export const GQL_MAINNET_CHAINS = [
export const CHAIN_ID_TO_BACKEND_NAME: { [key: number]: Chain } = { Chain.Ethereum,
Chain.Polygon,
Chain.Celo,
Chain.Optimism,
Chain.Arbitrum,
Chain.Bnb,
] as const
const GQL_TESTNET_CHAINS = [Chain.EthereumGoerli, Chain.EthereumSepolia] as const
const UX_SUPPORTED_GQL_CHAINS = [...GQL_MAINNET_CHAINS, ...GQL_TESTNET_CHAINS] as const
export type InterfaceGqlChain = typeof UX_SUPPORTED_GQL_CHAINS[number]
export const CHAIN_ID_TO_BACKEND_NAME: { [key: number]: InterfaceGqlChain } = {
[SupportedChainId.MAINNET]: Chain.Ethereum, [SupportedChainId.MAINNET]: Chain.Ethereum,
[SupportedChainId.GOERLI]: Chain.EthereumGoerli, [SupportedChainId.GOERLI]: Chain.EthereumGoerli,
[SupportedChainId.SEPOLIA]: Chain.EthereumSepolia,
[SupportedChainId.POLYGON]: Chain.Polygon, [SupportedChainId.POLYGON]: Chain.Polygon,
[SupportedChainId.POLYGON_MUMBAI]: Chain.Polygon, [SupportedChainId.POLYGON_MUMBAI]: Chain.Polygon,
[SupportedChainId.CELO]: Chain.Celo, [SupportedChainId.CELO]: Chain.Celo,
...@@ -100,13 +115,14 @@ export function gqlToCurrency(token: { ...@@ -100,13 +115,14 @@ export function gqlToCurrency(token: {
decimals?: number decimals?: number
name?: string name?: string
symbol?: string symbol?: string
}): Currency { }): Currency | undefined {
const chainId = fromGraphQLChain(token.chain) const chainId = supportedChainIdFromGQLChain(token.chain)
if (!chainId) return undefined
if (token.standard === TokenStandard.Native || !token.address) return nativeOnChain(chainId) if (token.standard === TokenStandard.Native || !token.address) return nativeOnChain(chainId)
else return new Token(chainId, token.address, token.decimals ?? 18, token.name, token.symbol) else return new Token(chainId, token.address, token.decimals ?? 18, token.name, token.symbol)
} }
const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: Chain } = { const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: InterfaceGqlChain } = {
ethereum: Chain.Ethereum, ethereum: Chain.Ethereum,
polygon: Chain.Polygon, polygon: Chain.Polygon,
celo: Chain.Celo, celo: Chain.Celo,
...@@ -119,8 +135,7 @@ export function validateUrlChainParam(chainName: string | undefined) { ...@@ -119,8 +135,7 @@ export function validateUrlChainParam(chainName: string | undefined) {
return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : Chain.Ethereum return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : Chain.Ethereum
} }
// TODO(cartcrom): refactor into safer lookup & replace usage const CHAIN_NAME_TO_CHAIN_ID: { [key in InterfaceGqlChain]: SupportedChainId } = {
export const CHAIN_NAME_TO_CHAIN_ID: { [key in Chain]: SupportedChainId } = {
[Chain.Ethereum]: SupportedChainId.MAINNET, [Chain.Ethereum]: SupportedChainId.MAINNET,
[Chain.EthereumGoerli]: SupportedChainId.GOERLI, [Chain.EthereumGoerli]: SupportedChainId.GOERLI,
[Chain.EthereumSepolia]: SupportedChainId.SEPOLIA, [Chain.EthereumSepolia]: SupportedChainId.SEPOLIA,
...@@ -128,15 +143,42 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key in Chain]: SupportedChainId } = { ...@@ -128,15 +143,42 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key in Chain]: SupportedChainId } = {
[Chain.Celo]: SupportedChainId.CELO, [Chain.Celo]: SupportedChainId.CELO,
[Chain.Optimism]: SupportedChainId.OPTIMISM, [Chain.Optimism]: SupportedChainId.OPTIMISM,
[Chain.Arbitrum]: SupportedChainId.ARBITRUM_ONE, [Chain.Arbitrum]: SupportedChainId.ARBITRUM_ONE,
[Chain.UnknownChain]: SupportedChainId.MAINNET,
[Chain.Bnb]: SupportedChainId.BNB, [Chain.Bnb]: SupportedChainId.BNB,
} }
export function fromGraphQLChain(chain: Chain): SupportedChainId { export function isSupportedGQLChain(chain: Chain): chain is InterfaceGqlChain {
return CHAIN_NAME_TO_CHAIN_ID[chain] return (UX_SUPPORTED_GQL_CHAINS as ReadonlyArray<Chain>).includes(chain)
}
export function supportedChainIdFromGQLChain(chain: InterfaceGqlChain): SupportedChainId
export function supportedChainIdFromGQLChain(chain: Chain): SupportedChainId | undefined
export function supportedChainIdFromGQLChain(chain: Chain): SupportedChainId | undefined {
return isSupportedGQLChain(chain) ? CHAIN_NAME_TO_CHAIN_ID[chain] : undefined
} }
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo] export function logSentryErrorForUnsupportedChain({
extras,
errorMessage,
}: {
extras?: Record<string, any>
errorMessage: string
}) {
Sentry.withScope((scope) => {
extras &&
Object.entries(extras).map(([k, v]) => {
scope.setExtra(k, v)
})
Sentry.captureException(new Error(errorMessage))
})
}
export const BACKEND_CHAIN_NAMES: InterfaceGqlChain[] = [
Chain.Ethereum,
Chain.Polygon,
Chain.Optimism,
Chain.Arbitrum,
Chain.Celo,
]
export function getTokenDetailsURL({ export function getTokenDetailsURL({
address, address,
......
...@@ -32,7 +32,7 @@ function buildRoutingItem(routingItem: NftTrade): RoutingItem { ...@@ -32,7 +32,7 @@ function buildRoutingItem(routingItem: NftTrade): RoutingItem {
} }
} }
function buildRoutingItems(routingItems: NftTrade[]): RoutingItem[] { function buildRoutingItems(routingItems: readonly NftTrade[]): RoutingItem[] {
return routingItems.map(buildRoutingItem) return routingItems.map(buildRoutingItem)
} }
......
...@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains' ...@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks' import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Chain } from 'graphql/data/Token' import { Chain } from 'graphql/data/Token'
import { fromGraphQLChain } from 'graphql/data/util' import { supportedChainIdFromGQLChain } from 'graphql/data/util'
export type CurrencyKey = string export type CurrencyKey = string
...@@ -21,8 +21,9 @@ export function currencyKeyFromGraphQL(contract: { ...@@ -21,8 +21,9 @@ export function currencyKeyFromGraphQL(contract: {
chain: Chain chain: Chain
standard?: TokenStandard standard?: TokenStandard
}): CurrencyKey { }): CurrencyKey {
const chainId = fromGraphQLChain(contract.chain) const chainId = supportedChainIdFromGQLChain(contract.chain)
const address = contract.standard === TokenStandard.Native ? NATIVE_CHAIN_ID : contract.address const address = contract.standard === TokenStandard.Native ? NATIVE_CHAIN_ID : contract.address
if (!address) throw new Error('Non-native token missing address') if (!address) throw new Error('Non-native token missing address')
if (!chainId) throw new Error('Unsupported chain from pools query')
return buildCurrencyKey(chainId, address) return buildCurrencyKey(chainId, address)
} }
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { Chain } from 'graphql/data/__generated__/types-and-hooks' import { Chain } from 'graphql/data/__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util' import { supportedChainIdFromGQLChain } from 'graphql/data/util'
export function getNativeTokenDBAddress(chain: Chain): string | undefined { export function getNativeTokenDBAddress(chain: Chain): string | undefined {
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain] const pageChainId = supportedChainIdFromGQLChain(chain)
if (pageChainId === undefined) { if (pageChainId === undefined) {
return undefined return undefined
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment