From 2b355a85a7050676d363b0de180a99c24d549938 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Wed, 14 Feb 2024 15:40:54 -0500 Subject: [PATCH 1/3] adding warning message to unverified tokens --- extension/package.json | 1 + extension/src/custom.d.ts | 2 + extension/src/popup/assets/icon-add.svg | 10 + extension/src/popup/assets/icon-remove.svg | 10 + .../popup/assets/icon-unverified-warning.svg | 5 + .../src/popup/assets/icon-unverified.svg | 5 + .../components/WarningMessages/index.tsx | 136 ++++++++++ .../components/WarningMessages/styles.scss | 95 +++++++ .../account/AccountAssets/index.tsx | 22 +- .../account/AccountAssets/styles.scss | 11 +- .../manageAssets/AddToken/index.tsx | 254 +++++++++++++----- .../manageAssets/AddToken/styles.scss | 13 + .../manageAssets/ChooseAsset/index.tsx | 2 +- .../manageAssets/ManageAssetRows/index.tsx | 243 +++++++++++------ .../manageAssets/ManageAssetRows/styles.scss | 13 + .../src/popup/ducks/transactionSubmission.ts | 3 - extension/src/popup/helpers/searchAsset.ts | 18 ++ yarn.lock | 5 + 18 files changed, 682 insertions(+), 166 deletions(-) create mode 100644 extension/src/popup/assets/icon-add.svg create mode 100644 extension/src/popup/assets/icon-remove.svg create mode 100644 extension/src/popup/assets/icon-unverified-warning.svg create mode 100644 extension/src/popup/assets/icon-unverified.svg create mode 100644 extension/src/popup/components/manageAssets/AddToken/styles.scss diff --git a/extension/package.json b/extension/package.json index 2966d7c7e1..370da6b78f 100644 --- a/extension/package.json +++ b/extension/package.json @@ -23,6 +23,7 @@ "@shared/api": "1.0.0", "@shared/constants": "1.0.0", "@shared/helpers": "1.0.0", + "@stellar-asset-lists/sdk": "^1.0.0", "@stellar/design-system": "^1.1.2", "@stellar/wallet-sdk": "v0.11.0-beta.1", "@testing-library/react": "^10.4.8", diff --git a/extension/src/custom.d.ts b/extension/src/custom.d.ts index 922155945e..d7005fbd5d 100644 --- a/extension/src/custom.d.ts +++ b/extension/src/custom.d.ts @@ -8,3 +8,5 @@ declare module "qrcode.react"; declare module "react-identicons"; declare module "stellar-identicon-js"; + +declare module "@stellar-asset-lists/sdk"; diff --git a/extension/src/popup/assets/icon-add.svg b/extension/src/popup/assets/icon-add.svg new file mode 100644 index 0000000000..b3e04b93f6 --- /dev/null +++ b/extension/src/popup/assets/icon-add.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extension/src/popup/assets/icon-remove.svg b/extension/src/popup/assets/icon-remove.svg new file mode 100644 index 0000000000..67b9c6fc5e --- /dev/null +++ b/extension/src/popup/assets/icon-remove.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extension/src/popup/assets/icon-unverified-warning.svg b/extension/src/popup/assets/icon-unverified-warning.svg new file mode 100644 index 0000000000..7415c8a763 --- /dev/null +++ b/extension/src/popup/assets/icon-unverified-warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/extension/src/popup/assets/icon-unverified.svg b/extension/src/popup/assets/icon-unverified.svg new file mode 100644 index 0000000000..a5bbfca83d --- /dev/null +++ b/extension/src/popup/assets/icon-unverified.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/extension/src/popup/components/WarningMessages/index.tsx b/extension/src/popup/components/WarningMessages/index.tsx index 03e9a7e301..fe87f4754b 100644 --- a/extension/src/popup/components/WarningMessages/index.tsx +++ b/extension/src/popup/components/WarningMessages/index.tsx @@ -10,6 +10,7 @@ import { Operation, Horizon, TransactionBuilder, + Networks, } from "stellar-sdk"; import { ActionStatus } from "@shared/api/types"; @@ -30,11 +31,13 @@ import { ManageAssetRow, NewAssetFlags, } from "popup/components/manageAssets/ManageAssetRows"; +import { SorobanTokenIcon } from "popup/components/account/AccountAssets"; import { View } from "popup/basics/layout/View"; import { useNetworkFees } from "popup/helpers/useNetworkFees"; import { publicKeySelector, hardwareWalletTypeSelector, + addTokenId, } from "popup/ducks/accountServices"; import { ROUTES } from "popup/constants/routes"; import { navigateTo } from "popup/helpers/navigate"; @@ -45,6 +48,7 @@ import { emitMetric } from "helpers/metrics"; import IconShieldCross from "popup/assets/icon-shield-cross.svg"; import IconInvalid from "popup/assets/icon-invalid.svg"; import IconWarning from "popup/assets/icon-warning.svg"; +import IconUnverifiedWarning from "popup/assets/icon-unverified-warning.svg"; import "./styles.scss"; import { INDEXER_URL } from "@shared/constants/mercury"; @@ -666,6 +670,138 @@ export const NewAssetWarning = ({ ); }; +export const UnverifiedTokenWarning = ({ + domain, + code, + issuer, + onClose, +}: { + domain: string; + code: string; + issuer: string; + onClose: () => void; +}) => { + const { t } = useTranslation(); + const dispatch: AppDispatch = useDispatch(); + const warningRef = useRef(null); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + const publicKey = useSelector(publicKeySelector); + const { submitStatus } = useSelector(transactionSubmissionSelector); + const [isSubmitting, setIsSubmitting] = useState(false); + + const closeOverlay = () => { + if (warningRef.current) { + warningRef.current.style.marginBottom = `-${POPUP_HEIGHT}px`; + } + const timeout = setTimeout(() => { + onClose(); + clearTimeout(timeout); + }, 300); + }; + + // animate entry + useEffect(() => { + if (warningRef.current) { + const timeout = setTimeout(() => { + // Adding extra check to fix flaky tests + if (warningRef.current) { + warningRef.current.style.marginBottom = "0"; + } + clearTimeout(timeout); + }, 10); + } + }, [warningRef]); + + const handleSubmit = async () => { + setIsSubmitting(true); + await dispatch( + addTokenId({ + publicKey, + tokenId: issuer, + network: networkDetails.network as Networks, + }), + ); + navigateTo(ROUTES.account); + + setIsSubmitting(false); + }; + + return ( +
+ +
+
+
+ +
+
{code}
+
{domain}
+
+
+ +
+
+ {t("Add Asset Trustline")} +
+
+
+ + +
+
+ {t("Asset Info")} +
+
+
+ unverified token +
+
+
+ {t("Unverified asset")} +
+
+ {t("Proceed with caution")} +
+
+
+
+ +
+
+ + +
{" "} +
+
+
+
+ ); +}; + export const TransferWarning = ({ operation, }: { diff --git a/extension/src/popup/components/WarningMessages/styles.scss b/extension/src/popup/components/WarningMessages/styles.scss index 3c257868a1..4e9c125785 100644 --- a/extension/src/popup/components/WarningMessages/styles.scss +++ b/extension/src/popup/components/WarningMessages/styles.scss @@ -258,6 +258,101 @@ } } +.UnverifiedTokenWarning { + z-index: var(--z-index--scam-warning); + display: flex; + position: fixed; + height: fit-content; + width: 100%; + bottom: -1.5rem; + left: 0; + + &__wrapper { + align-items: center; + z-index: calc(var(--z-index--scam-warning) + 1); + height: 100%; + margin-bottom: -600px; + display: flex; + flex-direction: column; + background: var(--color-gray-10); + padding: 2rem 1.5rem 1.5rem 1.5rem; + transition: margin var(--dropdown-animation); + border-top-right-radius: 1rem; + border-top-left-radius: 1rem; + } + + &__icon { + align-items: center; + display: flex; + justify-content: center; + margin-bottom: 0.5rem; + } + + &__code { + display: flex; + justify-content: center; + } + + &__description { + align-items: center; + background: var(--color-gray-40); + border-radius: 0.375rem; + display: flex; + gap: 0.25rem; + margin: 1rem 0 1.5rem 0; + padding: 0.125rem 0.375rem; + + &__icon { + svg { + color: var(--color-gray-70); + height: 0.75rem; + width: 0.75rem; + } + } + + &__text { + color: var(--color-gray-70); + font-size: 0.75rem; + line-height: 1.125rem; + } + } + + &__flags { + border-radius: 0.5rem; + margin-top: 0.5rem; + background: var(--color-gray-20); + padding: 0.75rem 1rem; + width: 100%; + + &__info { + font-size: 0.875rem; + color: var(--color-gray-70); + margin-bottom: 0.5rem; + } + } + + &__flag { + display: flex; + margin-bottom: 1.5rem; + column-gap: 0.5rem; + &__header { + color: var(--color-yellow-60); + font-size: 0.875rem; + margin-bottom: 0.25rem; + } + &__description { + color: var(--color-gray-70); + font-size: 0.75rem; + line-height: 1.25rem; + } + } + + &__bottom-content { + margin-top: 1rem; + width: 100%; + } +} + .TokenTransferWarning { overflow-wrap: break-word; diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index 22528baaa6..d4b3599755 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -21,6 +21,22 @@ import { formatAmount } from "popup/helpers/formatters"; const getIsXlm = (code: string) => code === "XLM"; +export const SorobanTokenIcon = ({ + code, + noMargin, +}: { + code: string; + noMargin?: boolean; +}) => ( +
+ {code.substring(0, 2)} +
+); + export const AssetIcon = ({ assetIcons, code, @@ -71,11 +87,7 @@ export const AssetIcon = ({ // Placeholder for Soroban tokens if (_isSorobanToken) { - return ( -
- S -
- ); + return ; } // If we're waiting on the icon lookup (Method 1), just return the loader until this re-renders with `assetIcons`. We can't do anything until we have it. diff --git a/extension/src/popup/components/account/AccountAssets/styles.scss b/extension/src/popup/components/account/AccountAssets/styles.scss index 0bbb91f3de..9035c02b01 100644 --- a/extension/src/popup/components/account/AccountAssets/styles.scss +++ b/extension/src/popup/components/account/AccountAssets/styles.scss @@ -26,16 +26,17 @@ $loader-light-color: #444961; &--lp-share, &--soroban-token { + border: 1px solid var(--color-gray-20); height: 32px; width: 32px; - background: var(--color-gray-10); border-radius: 2rem; display: flex; align-items: center; justify-content: center; - font-size: 1rem; + font-size: 0.875rem; + font-weight: var(--font-weight-semi-bold); line-height: 1.5rem; - color: var(--color-gray-90); + color: var(--color-gray-70); } &--error { @@ -67,6 +68,10 @@ $loader-light-color: #444961; height: 0; } } + + &--no-margin { + margin: 0; + } } @keyframes loadingAnimation { diff --git a/extension/src/popup/components/manageAssets/AddToken/index.tsx b/extension/src/popup/components/manageAssets/AddToken/index.tsx index a901b38984..78ca9ebc65 100644 --- a/extension/src/popup/components/manageAssets/AddToken/index.tsx +++ b/extension/src/popup/components/manageAssets/AddToken/index.tsx @@ -1,100 +1,208 @@ -import React from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { Button, Input } from "@stellar/design-system"; -import { Field, Form, Formik, FieldProps } from "formik"; +import React, { useEffect, useCallback, useRef, useState } from "react"; +import { useSelector } from "react-redux"; +import { Redirect } from "react-router-dom"; +import { Formik, Form, Field, FieldProps } from "formik"; +import { Icon, Input, Loader } from "@stellar/design-system"; +import debounce from "lodash/debounce"; import { useTranslation } from "react-i18next"; -import { Networks } from "stellar-sdk"; +import { INDEXER_URL } from "@shared/constants/mercury"; + +import { FormRows } from "popup/basics/Forms"; import { ROUTES } from "popup/constants/routes"; -import { METRIC_NAMES } from "popup/constants/metricsNames"; -import { AppDispatch } from "popup/App"; -import { navigateTo } from "popup/helpers/navigate"; -import { emitMetric } from "helpers/metrics"; -import { FormRows } from "popup/basics/Forms"; -import { View } from "popup/basics/layout/View"; +import { publicKeySelector } from "popup/ducks/accountServices"; +import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; +import { isCustomNetwork } from "helpers/stellar"; +import { searchToken } from "popup/helpers/searchAsset"; +import { isContractId } from "popup/helpers/soroban"; import { SubviewHeader } from "popup/components/SubviewHeader"; +import { View } from "popup/basics/layout/View"; +import IconUnverified from "popup/assets/icon-unverified.svg"; -import { - addTokenId, - authErrorSelector, - publicKeySelector, -} from "popup/ducks/accountServices"; -import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; +import { ManageAssetRows, ManageAssetCurrency } from "../ManageAssetRows"; +import "./styles.scss"; interface FormValues { - tokenId: string; + asset: string; } - const initialValues: FormValues = { - tokenId: "", + asset: "", +}; + +const VerificationBadge = ({ isVerified }: { isVerified: boolean }) => { + const { t } = useTranslation(); + + return ( +
+ {isVerified ? ( + <> + + {t("Verified")} + + ) : ( + <> + unverified icon + {t("Unverified")} + + )} +
+ ); }; export const AddToken = () => { const { t } = useTranslation(); - const dispatch: AppDispatch = useDispatch(); - const authError = useSelector(authErrorSelector); - const networkDetails = useSelector(settingsNetworkDetailsSelector); const publicKey = useSelector(publicKeySelector); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]); + const [isSearching, setIsSearching] = useState(false); + const [hasNoResults, setHasNoResults] = useState(false); + const [isVerifiedToken, setIsVerifiedToken] = useState(false); + const ResultsRef = useRef(null); + + interface TokenRecord { + code: string; + issuer: string; + contract: string; + org: string; + domain: string; + icon: string; + decimals: number; + } + + const handleSearch = useCallback( + debounce(async ({ target: { value: contractId } }) => { + if (!isContractId(contractId)) { + setAssetRows([]); + return; + } + setIsSearching(true); + + const verifiedTokenRes = await searchToken({ + networkDetails, + onError: (e) => { + console.error(e); + setIsSearching(false); + throw new Error(t("Unable to search for tokens")); + }, + }); + + const verifiedTokens = verifiedTokenRes.assets.filter( + (record: TokenRecord) => { + const regex = new RegExp(contractId, "i"); + if (record.contract.match(regex)) { + return true; + } + return false; + }, + ); - const handleSubmit = async (values: FormValues) => { - const { tokenId } = values; - const res = await dispatch( - addTokenId({ - publicKey, - tokenId, - network: networkDetails.network as Networks, - }), - ); - - if (addTokenId.fulfilled.match(res)) { - emitMetric(METRIC_NAMES.manageAssetAddToken); - navigateTo(ROUTES.account); - } - }; + setIsSearching(false); + + if (verifiedTokens.length) { + setIsVerifiedToken(true); + setAssetRows( + verifiedTokens.map((record: TokenRecord) => ({ + code: record.code, + issuer: record.contract, + image: record.icon, + domain: record.domain, + })), + ); + } else { + // lookup contract + setIsVerifiedToken(false); + const tokenUrl = new URL(`${INDEXER_URL}/token-details/${contractId}`); + tokenUrl.searchParams.append("network", networkDetails.network); + tokenUrl.searchParams.append("pub_key", publicKey); + tokenUrl.searchParams.append( + "soroban_url", + networkDetails.sorobanRpcUrl!, + ); + + const res = await fetch(tokenUrl.href); + const resJson = await res.json(); + + setAssetRows([ + { + code: resJson.symbol, + issuer: contractId, + domain: "", + name: resJson.name, + }, + ]); + } + }, 500), + [], + ); + + useEffect(() => { + setHasNoResults(!assetRows.length); + }, [assetRows]); + + if (isCustomNetwork(networkDetails)) { + return ; + } return ( - - {({ dirty, isSubmitting, isValid, errors, touched }) => ( - - -
+ {}}> + {({ dirty }) => ( + { + handleSearch(e); + setHasNoResults(false); + }} + > + + - - {({ field }: FieldProps) => ( - + + {({ field }: FieldProps) => ( + + )} + + +
+ {isSearching ? ( +
+ +
+ ) : null} + {assetRows.length ? ( + + ) : null} + + {assetRows.length ? ( + - )} - + ) : null} + {hasNoResults && dirty && !isSearching ? ( +
Token not found
+ ) : null} +
- - - - -
+
+ )}
); diff --git a/extension/src/popup/components/manageAssets/AddToken/styles.scss b/extension/src/popup/components/manageAssets/AddToken/styles.scss new file mode 100644 index 0000000000..5e0db7ed4b --- /dev/null +++ b/extension/src/popup/components/manageAssets/AddToken/styles.scss @@ -0,0 +1,13 @@ +.AddToken { + &__heading { + display: flex; + align-items: center; + margin: 0.5rem 0 1rem 0; + + &__text { + color: var(--color-gray-70); + font-size: 0.875rem; + margin-left: 0.15625rem; + } + } +} diff --git a/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx b/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx index 1b3b8f44c1..c3116ba442 100644 --- a/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx +++ b/extension/src/popup/components/manageAssets/ChooseAsset/index.tsx @@ -132,7 +132,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { ref={ManageAssetRowsWrapperRef} > {managingAssets ? ( - + ) : ( )} diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index ccbc1a523d..a732a6db9b 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from "react"; -import { StellarToml } from "stellar-sdk"; +import { StellarToml, Networks } from "stellar-sdk"; import { useDispatch, useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; +import { Button } from "@stellar/design-system"; import { ActionStatus } from "@shared/api/types"; import { AppDispatch } from "popup/App"; @@ -17,7 +18,6 @@ import { truncateString, } from "helpers/stellar"; -import { PillButton } from "popup/basics/buttons/PillButton"; import { LoadingBackground } from "popup/basics/LoadingBackground"; import { METRIC_NAMES } from "popup/constants/metricsNames"; @@ -25,6 +25,7 @@ import { ROUTES } from "popup/constants/routes"; import { publicKeySelector, hardwareWalletTypeSelector, + addTokenId, } from "popup/ducks/accountServices"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { @@ -43,6 +44,7 @@ import { HardwareSign } from "popup/components/hardwareConnect/HardwareSign"; import { ScamAssetWarning, NewAssetWarning, + UnverifiedTokenWarning, } from "popup/components/WarningMessages"; import { ScamAssetIcon } from "popup/components/account/ScamAssetIcon"; @@ -51,6 +53,8 @@ import { NETWORKS } from "@shared/constants/stellar"; import { getManageAssetXDR } from "popup/helpers/getManageAssetXDR"; import { checkForSuspiciousAsset } from "popup/helpers/checkForSuspiciousAsset"; import { isContractId } from "popup/helpers/soroban"; +import IconAdd from "popup/assets/icon-add.svg"; +import IconRemove from "popup/assets/icon-remove.svg"; export type ManageAssetCurrency = StellarToml.Api.Currency & { domain: string; @@ -66,18 +70,22 @@ interface ManageAssetRowsProps { children?: React.ReactNode; header?: React.ReactNode; assetRows: ManageAssetCurrency[]; + chooseAsset?: boolean; + isVerifiedToken?: boolean; } export const ManageAssetRows = ({ children, header, assetRows, + chooseAsset, + isVerifiedToken, }: ManageAssetRowsProps) => { const { t } = useTranslation(); const publicKey = useSelector(publicKeySelector); const networkDetails = useSelector(settingsNetworkDetailsSelector); const { - accountBalances: { balances = {} }, + accountBalances, submitStatus, hardwareWalletData: { status: hwStatus }, blockedDomains, @@ -85,9 +93,7 @@ export const ManageAssetRows = ({ const [assetSubmitting, setAssetSubmitting] = useState(""); const dispatch: AppDispatch = useDispatch(); const { recommendedFee } = useNetworkFees(); - const { accountBalanceStatus, tokensWithNoBalance } = useSelector( - tokensSelector, - ); + const { accountBalanceStatus } = useSelector(tokensSelector); const walletType = useSelector(hardwareWalletTypeSelector); const isHardwareWallet = !!walletType; @@ -95,6 +101,7 @@ export const ManageAssetRows = ({ false, ); const [showNewAssetWarning, setShowNewAssetWarning] = useState(false); + const [showUnverifiedWarning, setShowUnverifiedWarning] = useState(false); const [newAssetFlags, setNewAssetFlags] = useState({ isNewAsset: false, isInvalidDomain: false, @@ -238,17 +245,45 @@ export const ManageAssetRows = ({ }; const handleTokenRowClick = async ( - contractId: string, + assetRowData = { + code: "", + issuer: "", + domain: "", + image: "", + }, + isTrustlineActive: boolean, canonicalAsset?: string, ) => { + const contractId = assetRowData.issuer; setAssetSubmitting(canonicalAsset || contractId); - await dispatch( - removeTokenId({ - contractId, - network: networkDetails.network as NETWORKS, - }), - ); - navigateTo(ROUTES.account); + if (!isTrustlineActive) { + if (isVerifiedToken) { + await dispatch( + addTokenId({ + publicKey, + tokenId: contractId, + network: networkDetails.network as Networks, + }), + ); + navigateTo(ROUTES.account); + } else { + setSuspiciousAssetData({ + domain: assetRowData.domain, + code: assetRowData.code, + issuer: assetRowData.issuer, + image: assetRowData.image, + }); + setShowUnverifiedWarning(true); + } + } else { + await dispatch( + removeTokenId({ + contractId, + network: networkDetails.network as NETWORKS, + }), + ); + navigateTo(ROUTES.account); + } }; return ( @@ -279,78 +314,122 @@ export const ManageAssetRows = ({ }} /> )} + {showUnverifiedWarning && ( + { + setShowUnverifiedWarning(false); + }} + /> + )}
{header}
- {assetRows.map(({ code = "", domain, image = "", issuer = "" }) => { - if (!balances) return null; - const isContract = isContractId(issuer); - const canonicalAsset = getCanonicalFromAsset(code, issuer); - const isTrustlineActive = Object.keys(balances).some( - (balance) => balance === canonicalAsset, - ); - const isActionPending = - submitStatus === ActionStatus.PENDING || - accountBalanceStatus === ActionStatus.PENDING; - - return ( -
- -
- { - if (isContract) { - handleTokenRowClick(issuer, canonicalAsset); - } else { - handleRowClick( - { code, issuer, image, domain }, - isTrustlineActive, - ); + {assetRows.map( + ({ code = "", domain, image = "", issuer = "", name = "" }) => { + if (!accountBalances.balances) return null; + const isContract = isContractId(issuer); + const canonicalAsset = getCanonicalFromAsset(code, issuer); + const isTrustlineActive = + Object.keys(accountBalances.balances).some( + (balance) => balance === canonicalAsset, + ) || accountBalances.tokensWithNoBalance.includes(issuer); + const isActionPending = + submitStatus === ActionStatus.PENDING || + accountBalanceStatus === ActionStatus.PENDING; + return ( +
+ +
+ +
-
- ); - })} - - {tokensWithNoBalance.map((tokenId) => { - const isActionPending = - accountBalanceStatus === ActionStatus.PENDING; - - return ( -
- -
- handleTokenRowClick(tokenId)} - type="button" - > - {t("Remove")} - + ); + }, + )} + + {chooseAsset && + accountBalances.tokensWithNoBalance.map((code) => { + const isActionPending = + accountBalanceStatus === ActionStatus.PENDING; + + return ( +
+ +
+ +
-
- ); - })} + ); + })}
{children}
@@ -367,6 +446,7 @@ interface AssetRowData { issuer?: string; image?: string; domain: string; + name?: string; } export const ManageAssetRow = ({ @@ -374,6 +454,7 @@ export const ManageAssetRow = ({ issuer = "", image = "", domain, + name, }: AssetRowData) => { const { blockedDomains } = useSelector(transactionSubmissionSelector); const canonicalAsset = getCanonicalFromAsset(code, issuer); @@ -388,7 +469,7 @@ export const ManageAssetRow = ({ />
- {code} + {name || code}
{ state.accountBalances = action.payload; - state.tokensWithNoBalance = action.payload.tokensWithNoBalance; state.accountBalanceStatus = ActionStatus.SUCCESS; }); builder.addCase(getDestinationBalances.fulfilled, (state, action) => { diff --git a/extension/src/popup/helpers/searchAsset.ts b/extension/src/popup/helpers/searchAsset.ts index 63d1056c7a..c68e762d2b 100644 --- a/extension/src/popup/helpers/searchAsset.ts +++ b/extension/src/popup/helpers/searchAsset.ts @@ -1,3 +1,4 @@ +import { fetchAssetList } from "@stellar-asset-lists/sdk"; import { NetworkDetails } from "@shared/constants/stellar"; import { getApiStellarExpertUrl } from "popup/helpers/account"; @@ -19,3 +20,20 @@ export const searchAsset = async ({ return onError(e); } }; + +export const searchToken = async ({ + networkDetails, + onError, +}: { + networkDetails: NetworkDetails; + onError: (e: any) => void; +}) => { + try { + const res = await fetchAssetList( + `${getApiStellarExpertUrl(networkDetails)}/asset-list/top50`, + ); + return res; + } catch (e) { + return onError(e); + } +}; diff --git a/yarn.lock b/yarn.lock index 83e11874d7..1fe8ec4cd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3813,6 +3813,11 @@ p-map "^4.0.0" webpack-sources "^3.2.2" +"@stellar-asset-lists/sdk@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@stellar-asset-lists/sdk/-/sdk-1.0.0.tgz#59a70b07c01247ffed2f8d370d624ca2650c0b50" + integrity sha512-2RBdkUVStRbZZG7yxL6ow3B6cCjClSHe5bHF/NGKAkKR6MDXEfIvoLndsl4uWoLJTLfe3vdL9UX/T14p4INdBA== + "@stellar/design-system@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@stellar/design-system/-/design-system-1.1.2.tgz#09f27c13fef69975c6d9a214e770e419985c0b8e" From a1ae7e419f4865c511975c1c57bb88d54ccdf01b Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Wed, 14 Feb 2024 15:43:15 -0500 Subject: [PATCH 2/3] Added translations --- extension/src/popup/locales/en/translation.json | 13 +++++++++++-- extension/src/popup/locales/pt/translation.json | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 81a0a03692..9010ccf72e 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -21,12 +21,12 @@ "Add anyway": "Add anyway", "Add asset": "Add asset", "Add asset manually": "Add asset manually", + "Add Asset Trustline": "Add Asset Trustline", "Add custom network": "Add custom network", "Add Custom Network": "Add Custom Network", "Add it manually": "Add it manually", "Add network": "Add network", "Add New Address": "Add New Address", - "Add New Token": "Add New Token", "Add Soroban token": "Add Soroban token", "Addresses are uppercase and begin with letters “G“ or “M“": "Addresses are uppercase and begin with letters “G“ or “M“.", "ALL": "ALL", @@ -56,6 +56,7 @@ "Asset Code": "Asset Code", "Asset domain": "Asset domain", "Asset home domain doesn’t exist, TOML file format is invalid, or asset doesn't match currency description": "Asset home domain doesn’t exist, TOML file format is invalid, or asset doesn't match currency description", + "Asset Info": "Asset Info", "Asset not found": "Asset not found", "Assets found in this domain": "Assets found in this domain", "At least one uppercase letter": "At least one uppercase letter", @@ -74,6 +75,9 @@ "Base fee": "Base fee", "Before we start with migration, please read": "Before we start with migration, please read", "Before You Add This Asset": "Before You Add This Asset", + "Before you add this asset, please double-check its information and characteristics": { + " This can help you identify fraudulent assets": "Before you add this asset, please double-check its information and characteristics. This can help you identify fraudulent assets." + }, "Block malicious or unsafe addresses and domains": "Block malicious or unsafe addresses and domains", "Block trustlines to malicious or fraudulent assets": "Block trustlines to malicious or fraudulent assets", "Blocked asset": "Blocked asset", @@ -137,7 +141,6 @@ "Enter password": "Enter password", "Enter Password": "Enter Password", "Enter Soroban RPC URL": "Enter Soroban RPC URL", - "Enter Token ID": "Enter Token ID", "Enter your 12 word phrase to restore your wallet": "Enter your 12 word phrase to restore your wallet", "Enter your account password to authorize this transaction": "Enter your account password to authorize this transaction.", "Enter your account password to verify your account": "Enter your account password to verify your account.", @@ -331,6 +334,7 @@ "Preferences": "Preferences", "Price": "Price", "Privacy Policy": "Privacy Policy", + "Proceed with caution": "Proceed with caution", "Processing": "Processing", "Projects related to this asset may be fraudulent even if the creators say otherwise": { " ": "Projects related to this asset may be fraudulent even if the creators say otherwise. " @@ -441,6 +445,7 @@ "To": "To", "To create a new account you need to send at least 1 XLM to it": "To create a new account you need to send at least 1 XLM to it.", "To create this account, fund it with a minimum of 1 XLM": "To create this account, fund it with a minimum of 1 XLM.", + "Token ID": "Token ID", "Total Available": "Total Available", "Total Balance": "Total Balance", "Trading or sending this asset is not recommended": { @@ -460,12 +465,16 @@ "Unable to connect to": "Unable to connect to", "Unable to migrate": "Unable to migrate", "Unable to search for assets": "Unable to search for assets", + "Unable to search for tokens": "Unable to search for tokens", "Unsafe": "Unsafe", "Unsupported signing method": "Unsupported signing method", + "Unverified": "Unverified", + "Unverified asset": "Unverified asset", "Validate addresses that require a memo": "Validate addresses that require a memo", "Value": "Value", "Verification": "Verification", "Verification with": "Verification with", + "Verified": "Verified", "View on": "View on", "View public key": "View public key", "Wallet Address": "Wallet Address", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 786ac7444f..4c07e5773f 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -21,12 +21,12 @@ "Add anyway": "Add anyway", "Add asset": "Add asset", "Add asset manually": "Add asset manually", + "Add Asset Trustline": "Add Asset Trustline", "Add custom network": "Add custom network", "Add Custom Network": "Add Custom Network", "Add it manually": "Add it manually", "Add network": "Add network", "Add New Address": "Add New Address", - "Add New Token": "Add New Token", "Add Soroban token": "Add Soroban token", "Addresses are uppercase and begin with letters “G“ or “M“": "Addresses are uppercase and begin with letters “G“ or “M“.", "ALL": "TUDO", @@ -56,6 +56,7 @@ "Asset Code": "Asset Code", "Asset domain": "Asset domain", "Asset home domain doesn’t exist, TOML file format is invalid, or asset doesn't match currency description": "Asset home domain doesn’t exist, TOML file format is invalid, or asset doesn't match currency description", + "Asset Info": "Asset Info", "Asset not found": "Asset not found", "Assets found in this domain": "Assets found in this domain", "At least one uppercase letter": "At least one uppercase letter", @@ -74,6 +75,9 @@ "Base fee": "Base fee", "Before we start with migration, please read": "Before we start with migration, please read", "Before You Add This Asset": "Before You Add This Asset", + "Before you add this asset, please double-check its information and characteristics": { + " This can help you identify fraudulent assets": "Before you add this asset, please double-check its information and characteristics. This can help you identify fraudulent assets." + }, "Block malicious or unsafe addresses and domains": "Block malicious or unsafe addresses and domains", "Block trustlines to malicious or fraudulent assets": "Block trustlines to malicious or fraudulent assets", "Blocked asset": "Blocked asset", @@ -137,7 +141,6 @@ "Enter password": "Enter password", "Enter Password": "Enter Password", "Enter Soroban RPC URL": "Enter Soroban RPC URL", - "Enter Token ID": "Enter Token ID", "Enter your 12 word phrase to restore your wallet": "Enter your 12 word phrase to restore your wallet", "Enter your account password to authorize this transaction": "Enter your account password to authorize this transaction.", "Enter your account password to verify your account": "Enter your account password to verify your account.", @@ -331,6 +334,7 @@ "Preferences": "Preferences", "Price": "Price", "Privacy Policy": "Privacy Policy", + "Proceed with caution": "Proceed with caution", "Processing": "Processing", "Projects related to this asset may be fraudulent even if the creators say otherwise": { " ": "Projects related to this asset may be fraudulent even if the creators say otherwise. " @@ -441,6 +445,7 @@ "To": "To", "To create a new account you need to send at least 1 XLM to it": "To create a new account you need to send at least 1 XLM to it.", "To create this account, fund it with a minimum of 1 XLM": "To create this account, fund it with a minimum of 1 XLM.", + "Token ID": "Token ID", "Total Available": "Total Available", "Total Balance": "Total Balance", "Trading or sending this asset is not recommended": { @@ -460,12 +465,16 @@ "Unable to connect to": "Unable to connect to", "Unable to migrate": "Unable to migrate", "Unable to search for assets": "Unable to search for assets", + "Unable to search for tokens": "Unable to search for tokens", "Unsafe": "Unsafe", "Unsupported signing method": "Unsupported signing method", + "Unverified": "Unverified", + "Unverified asset": "Unverified asset", "Validate addresses that require a memo": "Validate addresses that require a memo", "Value": "Value", "Verification": "Verification", "Verification with": "Verification with", + "Verified": "Verified", "View on": "View on", "View public key": "View public key", "Wallet Address": "Wallet Address", From aab7d1207d1aea07c28eabaa9f5182486f5bacd8 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Wed, 14 Feb 2024 18:04:28 -0500 Subject: [PATCH 3/3] don't error on Freighter BE sending malformed response --- .../manageAssets/AddToken/index.tsx | 47 +++++++++++-------- .../manageAssets/AddToken/styles.scss | 7 +++ 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/extension/src/popup/components/manageAssets/AddToken/index.tsx b/extension/src/popup/components/manageAssets/AddToken/index.tsx index 78ca9ebc65..75ee5cf69d 100644 --- a/extension/src/popup/components/manageAssets/AddToken/index.tsx +++ b/extension/src/popup/components/manageAssets/AddToken/index.tsx @@ -113,25 +113,32 @@ export const AddToken = () => { } else { // lookup contract setIsVerifiedToken(false); - const tokenUrl = new URL(`${INDEXER_URL}/token-details/${contractId}`); - tokenUrl.searchParams.append("network", networkDetails.network); - tokenUrl.searchParams.append("pub_key", publicKey); - tokenUrl.searchParams.append( - "soroban_url", - networkDetails.sorobanRpcUrl!, - ); - - const res = await fetch(tokenUrl.href); - const resJson = await res.json(); - - setAssetRows([ - { - code: resJson.symbol, - issuer: contractId, - domain: "", - name: resJson.name, - }, - ]); + try { + const tokenUrl = new URL( + `${INDEXER_URL}/token-details/${contractId}`, + ); + tokenUrl.searchParams.append("network", networkDetails.network); + tokenUrl.searchParams.append("pub_key", publicKey); + tokenUrl.searchParams.append( + "soroban_url", + networkDetails.sorobanRpcUrl!, + ); + + const res = await fetch(tokenUrl.href); + const resJson = await res.json(); + + setAssetRows([ + { + code: resJson.symbol, + issuer: contractId, + domain: "", + name: resJson.name, + }, + ]); + } catch (e) { + setAssetRows([]); + console.error(e); + } } }, 500), [], @@ -196,7 +203,7 @@ export const AddToken = () => { /> ) : null} {hasNoResults && dirty && !isSearching ? ( -
Token not found
+
Token not found
) : null}
diff --git a/extension/src/popup/components/manageAssets/AddToken/styles.scss b/extension/src/popup/components/manageAssets/AddToken/styles.scss index 5e0db7ed4b..4f06c0facb 100644 --- a/extension/src/popup/components/manageAssets/AddToken/styles.scss +++ b/extension/src/popup/components/manageAssets/AddToken/styles.scss @@ -10,4 +10,11 @@ margin-left: 0.15625rem; } } + + &__not-found { + display: flex; + align-items: center; + margin: 0.5rem 0 1rem 0; + justify-content: center; + } }