diff --git a/.changeset/four-actors-obey.md b/.changeset/four-actors-obey.md new file mode 100644 index 00000000..dd38b656 --- /dev/null +++ b/.changeset/four-actors-obey.md @@ -0,0 +1,6 @@ +--- +'@reservoir0x/relay-sdk': patch +'@reservoir0x/relay-kit-ui': patch +--- + +Add support for unverified tokens diff --git a/packages/sdk/src/types/api.ts b/packages/sdk/src/types/api.ts index 6ea99aea..c8aab21f 100644 --- a/packages/sdk/src/types/api.ts +++ b/packages/sdk/src/types/api.ts @@ -4084,6 +4084,8 @@ export interface paths { limit?: number; /** @description Include all chains for a currency when filtering by chainId and address */ includeAllChains?: boolean; + /** @description Uses 3rd party API's to search for a token, in case relay does not have it indexed */ + useExternalSearch?: boolean; }; }; }; diff --git a/packages/ui/panda.config.ts b/packages/ui/panda.config.ts index 5f299982..c072bd69 100644 --- a/packages/ui/panda.config.ts +++ b/packages/ui/panda.config.ts @@ -57,6 +57,7 @@ export const Colors = { // Amber amber2: { value: '{colors.amber.2}' }, amber3: { value: '{colors.amber.3}' }, + amber4: { value: '{colors.amber.4}' }, amber9: { value: '{colors.amber.9}' }, amber10: { value: '{colors.amber.10}' }, amber11: { value: '{colors.amber.11}' }, diff --git a/packages/ui/src/components/common/CopyToClipBoard.tsx b/packages/ui/src/components/common/CopyToClipBoard.tsx new file mode 100644 index 00000000..64010f43 --- /dev/null +++ b/packages/ui/src/components/common/CopyToClipBoard.tsx @@ -0,0 +1,69 @@ +import { type FC, useState } from 'react' +import { Button, Text } from '../primitives/index.js' +import Tooltip from '../primitives/Tooltip.js' +import { useCopyToClipboard } from 'usehooks-ts' +import { AnimatePresence, motion } from 'framer-motion' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons' + +type CopyToClipBoardProps = { + text: string +} + +export const CopyToClipBoard: FC = ({ text }) => { + const [value, copy] = useCopyToClipboard() + const [isCopied, setIsCopied] = useState(false) + const [open, setOpen] = useState(false) + + const handleCopy = () => { + copy(text) + setIsCopied(true) + setTimeout(() => setIsCopied(false), 1000) + } + + return ( + {isCopied ? 'Copied!' : 'Copy'}} + > + + + ) +} diff --git a/packages/ui/src/components/common/Modal.tsx b/packages/ui/src/components/common/Modal.tsx index da9e23f9..c005ca65 100644 --- a/packages/ui/src/components/common/Modal.tsx +++ b/packages/ui/src/components/common/Modal.tsx @@ -15,6 +15,7 @@ import { AnimatePresence } from 'framer-motion' type ModalProps = { trigger?: ReactNode css?: SystemStyleObject + overlayZIndex?: number showCloseButton?: boolean children: ReactNode } @@ -26,7 +27,14 @@ export const Modal: FC< ComponentPropsWithoutRef, 'onPointerDownOutside' > -> = ({ trigger, css, showCloseButton = true, children, ...props }) => { +> = ({ + trigger, + css, + overlayZIndex = 9999, + showCloseButton = true, + children, + ...props +}) => { return ( {trigger} @@ -41,9 +49,9 @@ export const Modal: FC< left: 0, right: 0, bottom: 0, - zIndex: 9999, backgroundColor: 'blackA10' }} + style={{ zIndex: overlayZIndex }} > = ({ setToken, onAnalyticEvent }) => { + const [unverifiedTokenModalOpen, setUnverifiedTokenModalOpen] = + useState(false) + const [unverifiedToken, setUnverifiedToken] = useState() + const [internalOpen, setInternalOpen] = useState(false) const [open, setOpen] = openState || [internalOpen, setInternalOpen] const isSmallDevice = useMediaQuery('(max-width: 600px)') @@ -101,7 +110,10 @@ const TokenSelector: FC = ({ const relayClient = useRelayClient() const configuredChains = useMemo(() => { let chains = - relayClient?.chains.sort((a, b) => a.name.localeCompare(b.name)) ?? [] + relayClient?.chains.sort((a, b) => + a.displayName.localeCompare(b.displayName) + ) ?? [] + if (!multiWalletSupportEnabled && context === 'from') { chains = chains.filter((chain) => chain.vmType === 'evm') } @@ -153,6 +165,27 @@ const TokenSelector: FC = ({ } ) + const { data: externalTokenList, isLoading: isLoadingExternalTokenList } = + useTokenList( + relayClient?.baseApiUrl, + { + chainIds: chainFilter.id ? [chainFilter.id] : configuredChainIds, + address: isAddress(debouncedTokenSearchValue) + ? debouncedTokenSearchValue + : undefined, + term: !isAddress(debouncedTokenSearchValue) + ? debouncedTokenSearchValue + : undefined, + defaultList: false, + limit: 20, + ...(tokenListQuery ? { tokens: tokenListQuery } : {}), + useExternalSearch: true + }, + { + enabled: !!debouncedTokenSearchValue + } + ) + const { data: duneTokens, balanceMap: tokenBalances, @@ -207,20 +240,57 @@ const TokenSelector: FC = ({ enabled: duneTokenBalances ? true : false } ) + + const combinedTokenList = useMemo(() => { + if (!tokenList) return externalTokenList + if (!externalTokenList) return tokenList + + const mergedList = [...tokenList] + + externalTokenList.forEach((currencyList) => { + const externalCurrency = currencyList[0] + + if (externalCurrency) { + const alreadyExists = mergedList.some((list) => + list.some( + (existingCurrency) => + existingCurrency.chainId === externalCurrency.chainId && + existingCurrency?.address?.toLowerCase() === + externalCurrency?.address?.toLowerCase() + ) + ) + if (!alreadyExists) { + mergedList.push(currencyList) + } + } + }) + + return mergedList + }, [tokenList, externalTokenList]) + // Filter out unconfigured chains and append Relay Chain to each currency const enhancedCurrencyList = useMemo(() => { const _tokenList = - tokenList && (tokenList as any).length - ? (tokenList as CurrencyList[]) + combinedTokenList && (combinedTokenList as any).length + ? (combinedTokenList as CurrencyList[]) : undefined + + const filteredSuggestedTokens = chainFilter.id + ? suggestedTokens + ?.map((tokenList) => + tokenList.filter((token) => token.chainId === chainFilter.id) + ) + .filter((tokenList) => tokenList.length > 0) + : suggestedTokens + let list = context === 'from' && useDefaultTokenList && chainFilter.id === undefined && - suggestedTokens && - suggestedTokens.length > 0 - ? suggestedTokens - : tokenList + filteredSuggestedTokens && + filteredSuggestedTokens.length > 0 + ? filteredSuggestedTokens + : combinedTokenList const ethTokens = _tokenList?.find( (list) => list[0] && list[0].groupID === 'ETH' @@ -237,6 +307,7 @@ const TokenSelector: FC = ({ ) list = [ethTokens ?? [], usdcTokens ?? []].concat(list) } + const mappedList = list?.map((currencyList) => { const filteredList = currencyList .map((currency) => { @@ -295,12 +366,13 @@ const TokenSelector: FC = ({ .sort((a, b) => (b?.totalValueUsd ?? 0) - (a?.totalValueUsd ?? 0)) }, [ context, - tokenList, + combinedTokenList, suggestedTokens, useDefaultTokenList, configuredChains, tokenBalances, - multiWalletSupportEnabled + multiWalletSupportEnabled, + chainFilter ]) const isLoading = isLoadingSuggestedTokens || isLoadingTokenList @@ -397,9 +469,11 @@ const TokenSelector: FC = ({ symbol: currency.symbol, name: currency.name, decimals: currency.decimals, - logoURI: currency.metadata?.logoURI ?? '' + logoURI: currency.metadata?.logoURI ?? '', + verified: currency.metadata?.verified }) setOpen(false) + // reset state resetState() } @@ -434,82 +508,128 @@ const TokenSelector: FC = ({ }, [open]) return ( - { - onAnalyticEvent?.( - openChange - ? EventNames.SWAP_START_TOKEN_SELECT - : EventNames.SWAP_EXIT_TOKEN_SELECT, - { - type, - direction: context === 'from' ? 'input' : 'output' - } - ) - setOpen(openChange) - }} - showCloseButton={true} - trigger={trigger} - css={{ - p: '4', - sm: { - minWidth: - size === 'desktop' - ? !chainIdsFilter || chainIdsFilter.length > 1 - ? 568 - : 378 - : 400, - maxWidth: - size === 'desktop' && (!chainIdsFilter || chainIdsFilter.length > 1) - ? 568 - : 378 - } - }} - > - - {tokenSelectorStep === TokenSelectorStep.SetCurrency ? ( - +
+ { + onAnalyticEvent?.( + openChange + ? EventNames.SWAP_START_TOKEN_SELECT + : EventNames.SWAP_EXIT_TOKEN_SELECT, + { + type, + direction: context === 'from' ? 'input' : 'output' + } + ) + setOpen(openChange) + }} + showCloseButton={true} + trigger={trigger} + css={{ + p: '4', + sm: { + minWidth: + size === 'desktop' + ? !chainIdsFilter || chainIdsFilter.length > 1 + ? 568 + : 378 + : 400, + maxWidth: + size === 'desktop' && + (!chainIdsFilter || chainIdsFilter.length > 1) + ? 568 + : 378 + } + }} + > + + {tokenSelectorStep === TokenSelectorStep.SetCurrency ? ( + + ) : null} + {tokenSelectorStep === TokenSelectorStep.SetChain ? ( + + ) : null} + + +
+ + {unverifiedTokenModalOpen && ( + { + if (token) { + const currentData = getRelayUiKitData() + const tokenIdentifier = `${token.chainId}:${token.address}` + + if ( + !currentData.acceptedUnverifiedTokens.includes(tokenIdentifier) + ) { + setRelayUiKitData({ + acceptedUnverifiedTokens: [ + ...currentData.acceptedUnverifiedTokens, + tokenIdentifier + ] + }) + } + + selectToken(token, token.chainId) + onAnalyticEvent?.(EventNames.UNVERIFIED_TOKEN_ACCEPTED, { token }) } - token={token} - selectToken={selectToken} - setCurrencyList={setCurrencyList} - onAnalyticEvent={onAnalyticEvent} - /> - ) : null} - {tokenSelectorStep === TokenSelectorStep.SetChain ? ( - - ) : null} -
-
+ resetState() + setOpen(false) + setUnverifiedTokenModalOpen(false) + }} + /> + )} + ) } diff --git a/packages/ui/src/components/common/TokenSelector/steps/SetChainStep.tsx b/packages/ui/src/components/common/TokenSelector/steps/SetChainStep.tsx index 31ccf6c2..bb8e773c 100644 --- a/packages/ui/src/components/common/TokenSelector/steps/SetChainStep.tsx +++ b/packages/ui/src/components/common/TokenSelector/steps/SetChainStep.tsx @@ -12,6 +12,7 @@ import { import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faChevronLeft, + faExclamationTriangle, faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons' import { truncateAddress } from '../../../../utils/truncate.js' @@ -27,6 +28,7 @@ import type { RelayChain } from '@reservoir0x/relay-sdk' import { useMediaQuery } from 'usehooks-ts' import type { Token } from '../../../../types/index.js' import { solana } from '../../../../utils/solana.js' +import { getRelayUiKitData } from '../../../../utils/localStorage.js' import { bitcoin } from '../../../../utils/bitcoin.js' type SetChainStepProps = { @@ -37,6 +39,8 @@ type SetChainStepProps = { multiWalletSupportEnabled?: boolean setTokenSelectorStep: React.Dispatch> setInputElement: React.Dispatch> + setUnverifiedToken: React.Dispatch> + setUnverifiedTokenModalOpen: React.Dispatch> chainSearchInput: string setChainSearchInput: React.Dispatch> selectToken: (currency: Currency, chainId?: number) => void @@ -76,6 +80,8 @@ export const SetChainStep: FC = ({ chainSearchInput, setChainSearchInput, selectToken, + setUnverifiedToken, + setUnverifiedTokenModalOpen, selectedCurrencyList }) => { const client = useRelayClient() @@ -165,10 +171,27 @@ export const SetChainStep: FC = ({ : { ...chain.relayChain.currency, metadata: { - logoURI: `https://assets.relay.link/icons/currencies/${chain.relayChain.currency?.id}.png` + logoURI: `https://assets.relay.link/icons/currencies/${chain.relayChain.currency?.id}.png`, + verified: true } } - selectToken(token, chain.id) + + const isVerified = token?.metadata?.verified + if (!isVerified) { + const relayUiKitData = getRelayUiKitData() + const tokenKey = `${chain.id}:${token.address}` + const isAlreadyAccepted = + relayUiKitData.acceptedUnverifiedTokens.includes(tokenKey) + + if (isAlreadyAccepted) { + selectToken(token, chain.id) + } else { + setUnverifiedToken(token as Token) + setUnverifiedTokenModalOpen(true) + } + } else { + selectToken(token, chain.id) + } } } }} @@ -182,7 +205,6 @@ export const SetChainStep: FC = ({ pb: '2', gap: isDesktop ? '0' : '2', width: '100%', - scrollSnapType: 'y mandatory', scrollPaddingTop: '40px' }} > @@ -225,12 +247,28 @@ export const SetChainStep: FC = ({ {filteredChains?.map((chain) => { + const isSupported = chain.isSupported + const token = isSupported + ? { + ...chain.currency, + logoURI: chain.currency?.metadata?.logoURI + } + : { + ...chain.relayChain.currency, + logoURI: `https://assets.relay.link/icons/currencies/${chain.relayChain.currency?.id}.png`, + metadata: { + logoURI: `https://assets.relay.link/icons/currencies/${chain.relayChain.currency?.id}.png`, + verified: true + } + } + const decimals = chain?.currency?.balance?.decimals ?? 18 const compactBalance = Boolean( chain?.currency?.balance?.amount && decimals && chain?.currency.balance.amount.toString().length - decimals > 4 ) + const isVerified = token?.metadata?.verified return ( = ({ } }} > - - - {chain.displayName} - - {type === 'token' ? ( - - {truncateAddress(chain?.currency?.address)} - + + + + {chain.displayName} + {type === 'token' ? ( + + {truncateAddress(chain?.currency?.address)} + + ) : null} + + {!isVerified ? ( + + + ) : null} + {chain?.currency?.balance?.amount ? ( {formatBN( diff --git a/packages/ui/src/components/common/TokenSelector/steps/SetCurrencyStep.tsx b/packages/ui/src/components/common/TokenSelector/steps/SetCurrencyStep.tsx index 826b22ef..22ba7730 100644 --- a/packages/ui/src/components/common/TokenSelector/steps/SetCurrencyStep.tsx +++ b/packages/ui/src/components/common/TokenSelector/steps/SetCurrencyStep.tsx @@ -19,10 +19,12 @@ import { AccessibleListItem } from '../../../primitives/index.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons' -import { formatBN } from '../../../../utils/numbers.js' +import { + faExclamationTriangle, + faMagnifyingGlass +} from '@fortawesome/free-solid-svg-icons' +import { formatBN, formatDollar } from '../../../../utils/numbers.js' import { truncateAddress } from '../../../../utils/truncate.js' -import { LoadingSpinner } from '../../LoadingSpinner.js' import type { EnhancedCurrencyList } from '../TokenSelector.js' import type { Currency } from '@reservoir0x/relay-kit-hooks' import ChainFilter, { type ChainFilterValue } from '../../ChainFilter.js' @@ -31,6 +33,7 @@ import Fuse from 'fuse.js' import { useMediaQuery } from 'usehooks-ts' import type { Token } from '../../../../types/index.js' import { EventNames } from '../../../../constants/events.js' +import { getRelayUiKitData } from '../../../../utils/localStorage.js' type SetCurrencyProps = { size: 'mobile' | 'desktop' @@ -45,10 +48,13 @@ type SetCurrencyProps = { chainFilter: ChainFilterValue setChainFilter: (value: React.SetStateAction) => void isLoading: boolean + isLoadingExternalTokenList: boolean isLoadingDuneBalances: boolean enhancedCurrencyList?: EnhancedCurrencyList[] token?: Token selectToken: (currency: Currency, chainId?: number) => void + setUnverifiedTokenModalOpen: React.Dispatch> + setUnverifiedToken: React.Dispatch> setCurrencyList: (currencyList: EnhancedCurrencyList) => void onAnalyticEvent?: (eventName: string, data?: any) => void } @@ -71,9 +77,12 @@ export const SetCurrencyStep: FC = ({ chainFilter, setChainFilter, isLoading, + isLoadingExternalTokenList, isLoadingDuneBalances, enhancedCurrencyList, selectToken, + setUnverifiedTokenModalOpen, + setUnverifiedToken, setCurrencyList, onAnalyticEvent }) => { @@ -105,6 +114,37 @@ export const SetCurrencyStep: FC = ({ } }, [filteredChains, chainFilter]) + const handleCurrencySelection = (currencyList: EnhancedCurrencyList) => { + if (currencyList.chains.length > 1) { + setCurrencyList(currencyList) + } else { + const token = { + ...currencyList.chains[0], + logoURI: currencyList.chains[0].metadata?.logoURI + } + + const isVerified = currencyList.chains[0].metadata?.verified + + if (!isVerified) { + const relayUiKitData = getRelayUiKitData() + const tokenKey = `${token.chainId}:${token.address}` + const isAlreadyAccepted = + relayUiKitData.acceptedUnverifiedTokens.includes(tokenKey) + + if (isAlreadyAccepted) { + selectToken(token, token.chainId) + setCurrencyList(currencyList) + } else { + setUnverifiedToken(token as Token) + setUnverifiedTokenModalOpen(true) + } + } else { + selectToken(token, token.chainId) + setCurrencyList(currencyList) + } + } + } + return ( <> = ({ gap: '1', height: '100%', overflowY: 'auto', - scrollSnapType: 'y mandatory', scrollPaddingTop: '40px' }} > @@ -276,17 +315,14 @@ export const SetCurrencyStep: FC = ({ onSelect={(value) => { if (value && value !== 'input') { const selectedCurrency = enhancedCurrencyList?.find((list) => - list?.chains.some((chain) => chain.address === value) + list.chains.some((chain) => + value.includes(':') + ? `${chain.chainId}:${chain.address}` === value + : chain.address === value + ) ) if (selectedCurrency) { - if (selectedCurrency.chains.length === 1) { - selectToken( - selectedCurrency.chains[0], - selectedCurrency.chains[0].chainId - ) - } else { - setCurrencyList(selectedCurrency) - } + handleCurrencySelection(selectedCurrency) } } }} @@ -297,7 +333,6 @@ export const SetCurrencyStep: FC = ({ gap: '1', height: '100%', overflowY: 'auto', - scrollSnapType: 'y mandatory', scrollPaddingTop: '40px' }} > @@ -357,37 +392,59 @@ export const SetCurrencyStep: FC = ({ ) : null} - {/* Loading State*/} - {isLoading ? ( - - - - ) : null} {/* Data State */} {!isLoading && enhancedCurrencyList && enhancedCurrencyList?.length > 0 - ? enhancedCurrencyList?.map((list, idx) => - list && list.chains[0].address ? ( - - - - ) : null - ) + ? enhancedCurrencyList?.map((list, idx) => { + if (list && list.chains[0]?.address) { + const value = + list.chains.length === 1 + ? `${list.chains[0].chainId}:${list.chains[0].address}` + : list.chains[0].address + + return ( + + + + ) + } + }) : null} + + {/* Loading State*/} + {isLoading || isLoadingExternalTokenList ? ( + <> + {Array.from({ length: 5 }).map((_, index) => ( + + + + + + + + ))} + + ) : null} + {/* Empty State */} {!isLoading && + !isLoadingExternalTokenList && (!enhancedCurrencyList || enhancedCurrencyList?.length === 0) ? ( No results found. @@ -402,22 +459,16 @@ export const SetCurrencyStep: FC = ({ type CurrencyRowProps = { currencyList: EnhancedCurrencyList - setCurrencyList: (currencyList: EnhancedCurrencyList) => void - selectToken: (currency: Currency, chainId?: number) => void isLoadingDuneBalances: boolean + handleCurrencySelection: (currencyList: EnhancedCurrencyList) => void } const CurrencyRow = forwardRef( ( - { - currencyList, - setCurrencyList, - selectToken, - isLoadingDuneBalances, - ...props - }, + { currencyList, isLoadingDuneBalances, handleCurrencySelection, ...props }, ref ) => { + const totalValueUsd = currencyList.totalValueUsd const balance = currencyList.totalBalance const decimals = currencyList?.chains?.length > 0 @@ -428,26 +479,16 @@ const CurrencyRow = forwardRef( ) const isSingleChainCurrency = currencyList?.chains?.length === 1 + const isVerified = currencyList?.chains?.[0].metadata?.verified return ( ) } diff --git a/packages/ui/src/components/common/TokenSelector/triggers/SwapWidgetTokenTrigger.tsx b/packages/ui/src/components/common/TokenSelector/triggers/SwapWidgetTokenTrigger.tsx index 96e3a1d9..86a538e2 100644 --- a/packages/ui/src/components/common/TokenSelector/triggers/SwapWidgetTokenTrigger.tsx +++ b/packages/ui/src/components/common/TokenSelector/triggers/SwapWidgetTokenTrigger.tsx @@ -7,7 +7,7 @@ import { Box } from '../../../../components/primitives/index.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faChevronDown } from '@fortawesome/free-solid-svg-icons' +import { faChevronDown, faCoins } from '@fortawesome/free-solid-svg-icons' type SwapWidgetTokenTriggerProps = { token?: Token @@ -18,6 +18,8 @@ export const SwapWidgetTokenTrigger: FC = ({ token, locked }) => { + const isValidTokenLogo = token?.logoURI && token.logoURI !== 'missing.png' + return token ? ( + + + + + + ) +} diff --git a/packages/ui/src/components/primitives/Button.tsx b/packages/ui/src/components/primitives/Button.tsx index c10a239b..613bf06d 100644 --- a/packages/ui/src/components/primitives/Button.tsx +++ b/packages/ui/src/components/primitives/Button.tsx @@ -86,10 +86,10 @@ const ButtonCss = cva({ } }, warning: { - backgroundColor: 'amber2', + backgroundColor: 'amber3', color: 'amber11', '&:hover': { - backgroundColor: 'amber3', + backgroundColor: 'amber4', color: 'amber11' } } diff --git a/packages/ui/src/components/primitives/ChainTokenIcon.tsx b/packages/ui/src/components/primitives/ChainTokenIcon.tsx index 0d6b86b9..f5722384 100644 --- a/packages/ui/src/components/primitives/ChainTokenIcon.tsx +++ b/packages/ui/src/components/primitives/ChainTokenIcon.tsx @@ -1,7 +1,10 @@ import { type FC } from 'react' import Flex from './Flex.js' import ChainIcon from './ChainIcon.js' +import Box from './Box.js' import type { Styles } from '@reservoir0x/relay-design-system/css' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCoins } from '@fortawesome/free-solid-svg-icons' type ChainTokenProps = { chainId?: number @@ -14,17 +17,37 @@ export const ChainTokenIcon: FC = ({ tokenlogoURI, css = {} }) => { - return chainId && tokenlogoURI ? ( + const isValidTokenLogo = tokenlogoURI && tokenlogoURI !== 'missing.png' + + return chainId ? ( - {'Token'} + {isValidTokenLogo ? ( + {'Token'} + ) : ( + + + + )} ) => { const response = (useQuery as QueryType)({ queryKey: ['useDuneBalances', address], queryFn: () => { - let url = `https://api.dune.com/api/beta/balance/${address?.toLowerCase()}?chain_ids=all` + let url = `https://api.dune.com/api/beta/balance/${address?.toLowerCase()}?chain_ids=all&exclude_spam_tokens=true` if (isSvmAddress) { - url = `https://api.dune.com/api/beta/balance/solana/${address}?chain_ids=all` + url = `https://api.dune.com/api/beta/balance/solana/${address}?chain_ids=all&exclude_spam_tokens=true` } if (isBvmAddress) { @@ -108,24 +108,21 @@ export default (address?: string, queryOptions?: Partial) => { } }) - const balanceMap = response?.data?.balances?.reduce( - (balanceMap, balance) => { - if (balance.address === 'native') { - balance.address = - balance.chain === 'solana' - ? '11111111111111111111111111111111' - : zeroAddress - } - let chainId = balance.chain_id - if (!chainId && balance.chain === 'solana') { - chainId = solana.id - } + const balanceMap = response?.data?.balances?.reduce((balanceMap, balance) => { + if (balance.address === 'native') { + balance.address = + balance.chain === 'solana' + ? '11111111111111111111111111111111' + : zeroAddress + } + let chainId = balance.chain_id + if (!chainId && balance.chain === 'solana') { + chainId = solana.id + } - balanceMap[`${chainId}:${balance.address}`] = balance - return balanceMap - }, - {} as Record['balances'][0]> - ) + balanceMap[`${chainId}:${balance.address}`] = balance + return balanceMap + }, {} as Record['balances'][0]>) return { ...response, balanceMap, queryKey } as ReturnType & { balanceMap: typeof balanceMap diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index ac42de9c..fbc78296 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -9,6 +9,7 @@ type Token = { symbol: string decimals: number logoURI: string + verified?: boolean } type LinkedWallet = { diff --git a/packages/ui/src/utils/localStorage.ts b/packages/ui/src/utils/localStorage.ts new file mode 100644 index 00000000..eafc65dd --- /dev/null +++ b/packages/ui/src/utils/localStorage.ts @@ -0,0 +1,20 @@ +const RELAY_UI_KIT_KEY = 'relayUiKitData' + +interface RelayUiKitData { + acceptedUnverifiedTokens: string[] +} + +export function getRelayUiKitData(): RelayUiKitData { + if (typeof window === 'undefined') return { acceptedUnverifiedTokens: [] } + + const storedData = localStorage.getItem(RELAY_UI_KIT_KEY) + return storedData ? JSON.parse(storedData) : { acceptedUnverifiedTokens: [] } +} + +export function setRelayUiKitData(newData: Partial): void { + if (typeof window === 'undefined') return + + const currentData = getRelayUiKitData() + const updatedData = { ...currentData, ...newData } + localStorage.setItem(RELAY_UI_KIT_KEY, JSON.stringify(updatedData)) +}