From 28bec0b2e6622da4915645df263235ffb282f4d2 Mon Sep 17 00:00:00 2001 From: Matias Poblete <86752543+MattPoblete@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:17:21 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Add=20unsafe=20token=20modal=20and=20c?= =?UTF-8?q?ontractID=20validation=20for=20potentially=20unsafe=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SearchModal/CurrencySearch.tsx | 22 +++++++- .../UnsafeTokenModal/UnsafeTokenModal.tsx | 50 +++++++++++++++++++ src/hooks/tokens/useToken.ts | 15 ++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/components/UnsafeTokenModal/UnsafeTokenModal.tsx diff --git a/src/components/SearchModal/CurrencySearch.tsx b/src/components/SearchModal/CurrencySearch.tsx index 437c4da5..9a52b284 100644 --- a/src/components/SearchModal/CurrencySearch.tsx +++ b/src/components/SearchModal/CurrencySearch.tsx @@ -29,6 +29,7 @@ import Column from '../Column'; import Row, { RowBetween } from '../Row'; import CurrencyList, { CurrencyRow, formatAnalyticsEventProperties } from './CurrencyList'; import { PaddedColumn, SearchInput, Separator } from './styleds'; +import UnsafeTokenModalContent from 'components/UnsafeTokenModal/UnsafeTokenModal'; const ContentWrapper = styled(Column)<{ modalheight?: number }>` overflow: hidden; @@ -86,10 +87,11 @@ export function CurrencySearch({ const [searchQuery, setSearchQuery] = useState(''); const debouncedQuery = useDebounce(searchQuery, 200); const isAddressSearch = isAddress(debouncedQuery); - const { token: searchToken, needsWrapping, isLoading: isLoadingToken } = useToken(debouncedQuery); + const { token: searchToken, needsWrapping, isLoading: isLoadingToken, tokenIsSafe } = useToken(debouncedQuery); const { tokensAsMap: allTokens } = useAllTokens(); const [showUserAddedTokenModal, setShowUserAddedTokenModal] = useState(false); + const [showUnsafeTokenModal, setShowUnsafeTokenModal] = useState(false); const [showWrapStellarAssetModal, setShowWrapStellarAssetModal] = useState(false); //This sends and event when a token is searched to google analytics @@ -129,6 +131,11 @@ export function CurrencySearch({ const searchCurrencies = useSortTokensByQuery(debouncedQuery, filteredTokens); + const handleConfirmUnsafeToken = () => { + setShowUnsafeTokenModal(false); + setShowUserAddedTokenModal(true); + } + const handleConfirmAddUserToken = () => { if (!searchToken) return; addUserToken(searchToken, sorobanContext); @@ -144,9 +151,12 @@ export function CurrencySearch({ (currency: TokenType, hasWarning?: boolean) => { if (needsWrapping) { setShowWrapStellarAssetModal(true); + } else if (!allTokens[currency.contract] && !tokenIsSafe) { + setShowUnsafeTokenModal(true); } else if (!allTokens[currency.contract]) { setShowUserAddedTokenModal(true); - } else { + } + else { onCurrencySelect(currency, hasWarning); if (!hasWarning) onDismiss(); } @@ -193,6 +203,14 @@ export function CurrencySearch({ return ( <> + setShowUnsafeTokenModal(false)}> +
+ setShowUnsafeTokenModal(false)} + isSafe={tokenIsSafe} /> +
+
setShowUserAddedTokenModal(false)}>
void; + onConfirm: () => void; + isSafe: boolean | undefined; +} + +const UnsafeTokenModalContent = ({ onDismiss, onConfirm, isSafe }: Props) => { + const theme = useTheme(); + return ( + + + +
+ + + + + + + + Unsafe token! + + {isSafe === false && ( + + The chosen token has been identified as potentially unsafe due to a mismatch between its contract ID and the expected CODE:ISSUER combination indicated by its name. + + )} + {isSafe === undefined && ( + + This token issuer can't be verified. + + )} + + Accept risk + + + ); +}; + +export default UnsafeTokenModalContent; diff --git a/src/hooks/tokens/useToken.ts b/src/hooks/tokens/useToken.ts index f093ca6c..645e95bb 100644 --- a/src/hooks/tokens/useToken.ts +++ b/src/hooks/tokens/useToken.ts @@ -7,6 +7,7 @@ import useSWR, { useSWRConfig } from 'swr'; import useSWRImmutable from 'swr/immutable'; import { useAllTokens } from './useAllTokens'; import { getToken, isClassicStellarAsset } from './utils'; +import { Asset } from '@stellar/stellar-sdk'; export const findToken = async ( tokenAddress: string | undefined, @@ -65,6 +66,7 @@ const revalidateKeysCondition = (key: any) => { //Returns token from map (user added + api) or network export function useToken(tokenAddress: string | undefined) { const sorobanContext = useSorobanReact(); + const { activeChain: network } = sorobanContext; const { tokensAsMap } = useAllTokens(); const { data: name } = useSWR( @@ -95,6 +97,18 @@ export function useToken(tokenAddress: string | undefined) { const needsWrapping = !data && isStellarClassicAsset; + const checkContractId = (contractId: string, code: string, issuer: string): boolean | undefined => { + if (!issuer) { + return undefined; + } + const asset = new Asset(code, issuer); + if (asset.contractId(network?.networkPassphrase!) === contractId) { + return true; + } else { + return false; + } + } + const isSafe = data ? checkContractId(data.contract, data.code, data.issuer!) : false; const needsWrappingOnAddLiquidity = (!data && isStellarClassicAsset) || !name; @@ -120,6 +134,7 @@ export function useToken(tokenAddress: string | undefined) { //if not data and AssetExists return isWrapped: false return { token: data ?? newTokenData, + tokenIsSafe: isSafe, needsWrapping, isLoading: bothLoading, isError: error,