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

✨Add unsafe token modal and contractID validation #541

Merged
merged 1 commit into from
Sep 13, 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
22 changes: 20 additions & 2 deletions src/components/SearchModal/CurrencySearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -86,10 +87,11 @@ export function CurrencySearch({
const [searchQuery, setSearchQuery] = useState<string>('');
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<boolean>(false);
const [showUnsafeTokenModal, setShowUnsafeTokenModal] = useState<boolean>(false);
const [showWrapStellarAssetModal, setShowWrapStellarAssetModal] = useState<boolean>(false);

//This sends and event when a token is searched to google analytics
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
Expand Down Expand Up @@ -193,6 +203,14 @@ export function CurrencySearch({

return (
<>
<Modal open={showUnsafeTokenModal} onClose={() => setShowUnsafeTokenModal(false)}>
<div>
<UnsafeTokenModalContent
onConfirm={handleConfirmUnsafeToken}
onDismiss={() => setShowUnsafeTokenModal(false)}
isSafe={tokenIsSafe} />
</div>
</Modal>
<Modal open={showUserAddedTokenModal} onClose={() => setShowUserAddedTokenModal(false)}>
<div>
<UserAddedTokenModalContent
Expand Down
50 changes: 50 additions & 0 deletions src/components/UnsafeTokenModal/UnsafeTokenModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

import { Box, useTheme } from 'soroswap-ui';
import { ButtonPrimary } from 'components/Buttons/Button';
import { CloseButton } from 'components/Buttons/CloseButton';
import { AutoColumn } from 'components/Column';
import { RowBetween } from 'components/Row';
import { SubHeaderLarge, SubHeaderSmall } from 'components/Text';
import { Wrapper } from 'components/TransactionConfirmationModal/ModalStyles';
import { AlertTriangle } from 'react-feather';

interface Props {
onDismiss: () => void;
onConfirm: () => void;
isSafe: boolean | undefined;
}

const UnsafeTokenModalContent = ({ onDismiss, onConfirm, isSafe }: Props) => {
const theme = useTheme();
return (
<Wrapper>
<AutoColumn gap="12px">
<RowBetween>
<div />
<CloseButton onClick={onDismiss} />
</RowBetween>
<Box display="flex" justifyContent="center">
<AlertTriangle size="64px" stroke={theme.palette.customBackground.accentCritical} />
</Box>
<AutoColumn gap="12px" justify="center">
<SubHeaderLarge color="textPrimary" textAlign="center">
Unsafe token!
</SubHeaderLarge>
{isSafe === false && (
<SubHeaderSmall color={"textSecondary"} textAlign="center" marginBottom="12px">
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.
</SubHeaderSmall>
)}
{isSafe === undefined && (
<SubHeaderSmall color={"textSecondary"} textAlign="center" marginBottom="12px">
This token issuer can&apos;t be verified.
</SubHeaderSmall>
)}
</AutoColumn>
<ButtonPrimary onClick={onConfirm}>Accept risk</ButtonPrimary>
</AutoColumn>
</Wrapper>
);
};

export default UnsafeTokenModalContent;
15 changes: 15 additions & 0 deletions src/hooks/tokens/useToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;

Expand All @@ -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,
Expand Down