From 4edcb81745cd6bcfc74eb47ad79f47edc474e07f Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Tue, 27 Aug 2024 12:21:30 -0600 Subject: [PATCH 01/67] adds fetchJson to handle non json fetch responses --- extension/src/popup/helpers/blockaid.ts | 20 +++++++++++--------- extension/src/popup/helpers/fetch.ts | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 extension/src/popup/helpers/fetch.ts diff --git a/extension/src/popup/helpers/blockaid.ts b/extension/src/popup/helpers/blockaid.ts index 581bae49b..9fbb995ff 100644 --- a/extension/src/popup/helpers/blockaid.ts +++ b/extension/src/popup/helpers/blockaid.ts @@ -7,6 +7,7 @@ import { isCustomNetwork } from "@shared/helpers/stellar"; import { isMainnet } from "helpers/stellar"; import { emitMetric } from "helpers/metrics"; import { METRIC_NAMES } from "popup/constants/metricsNames"; +import { fetchJson } from "./fetch"; interface BlockAidScanSiteResult { status: "hit" | "miss"; @@ -84,25 +85,26 @@ export const useScanTx = () => { setLoading(false); return; } - const res = await fetch( + const response = await fetchJson<{ + data: BlockAidScanTxResult; + error: string | null; + }>( `${INDEXER_URL}/scan-tx?url=${encodeURIComponent( url, )}&tx_xdr=${xdr}&network=${networkDetails.network}`, ); - const response = (await res.json()) as { - data: BlockAidScanTxResult; - error: string | null; - }; - if (!res.ok) { - setError(response.error || "Failed to scan transaction"); - } setData(response.data); emitMetric(METRIC_NAMES.blockaidTxScan, { response: response.data }); setLoading(false); } catch (err) { setError("Failed to scan transaction"); - Sentry.captureException(err); + Sentry.captureException({ + error: err, + xdr, + url, + networkDetails, + }); setLoading(false); } }; diff --git a/extension/src/popup/helpers/fetch.ts b/extension/src/popup/helpers/fetch.ts new file mode 100644 index 000000000..b904912e9 --- /dev/null +++ b/extension/src/popup/helpers/fetch.ts @@ -0,0 +1,14 @@ +export const fetchJson = async (url: string, options?: RequestInit) => { + const res = await fetch(url, options); + if (!res.ok) { + throw new Error(res.statusText); + } + + if (res.headers.get("content-type") !== "application/json") { + const content = await res.text(); + throw new Error(content); + } + + const data = res.json() as T; + return data; +}; From 599e250e1ad7f0d18206ec4ee6a198d99c459f25 Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Tue, 27 Aug 2024 12:26:23 -0600 Subject: [PATCH 02/67] awaits json response --- extension/src/popup/helpers/fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/helpers/fetch.ts b/extension/src/popup/helpers/fetch.ts index b904912e9..bee0aa0cd 100644 --- a/extension/src/popup/helpers/fetch.ts +++ b/extension/src/popup/helpers/fetch.ts @@ -9,6 +9,6 @@ export const fetchJson = async (url: string, options?: RequestInit) => { throw new Error(content); } - const data = res.json() as T; + const data = (await res.json()) as T; return data; }; From b5496c9e0f13e52ee14786f153b8e9264367b4e8 Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Tue, 27 Aug 2024 13:06:24 -0600 Subject: [PATCH 03/67] encodes xdr for query param usage --- extension/src/popup/helpers/blockaid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/helpers/blockaid.ts b/extension/src/popup/helpers/blockaid.ts index 9fbb995ff..c8ef368d2 100644 --- a/extension/src/popup/helpers/blockaid.ts +++ b/extension/src/popup/helpers/blockaid.ts @@ -91,7 +91,7 @@ export const useScanTx = () => { }>( `${INDEXER_URL}/scan-tx?url=${encodeURIComponent( url, - )}&tx_xdr=${xdr}&network=${networkDetails.network}`, + )}&tx_xdr=${encodeURIComponent(xdr)}&network=${networkDetails.network}`, ); setData(response.data); From 9b758dbed8b9eee3a3f1325c88f047d5aed30822 Mon Sep 17 00:00:00 2001 From: aristides Date: Wed, 28 Aug 2024 09:33:19 -0600 Subject: [PATCH 04/67] [FEATURE] malicious tx warning, sign-tx (#1449) * adds response handling and warning for malicious case of tx scan * tweaks malicious tx warning copy * use internal hook loading and data vars * adds warning variant to tx warning * Added translations * restore initial null value for tx scan data --- .../components/WarningMessages/index.tsx | 30 ++++++++++++++++++ extension/src/popup/helpers/blockaid.ts | 31 +++++++++++++------ .../src/popup/locales/en/translation.json | 4 +-- .../src/popup/locales/pt/translation.json | 4 +-- .../src/popup/views/SignTransaction/index.tsx | 10 ++++-- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/extension/src/popup/components/WarningMessages/index.tsx b/extension/src/popup/components/WarningMessages/index.tsx index 5d2391ca8..f0d12c3d7 100644 --- a/extension/src/popup/components/WarningMessages/index.tsx +++ b/extension/src/popup/components/WarningMessages/index.tsx @@ -54,6 +54,7 @@ import IconWarning from "popup/assets/icon-warning.svg"; import IconUnverified from "popup/assets/icon-unverified.svg"; import IconNewAsset from "popup/assets/icon-new-asset.svg"; import { getVerifiedTokens } from "popup/helpers/searchAsset"; +import { BlockAidScanTxResult } from "popup/helpers/blockaid"; import { CopyValue } from "../CopyValue"; import "./styles.scss"; @@ -1104,3 +1105,32 @@ export const BlockAidMissWarning = () => { ); }; + +export const BlockaidMaliciousTxWarning = ({ + type, +}: { + type: BlockAidScanTxResult["validation"]["result_type"]; +}) => { + const { t } = useTranslation(); + const details = + type === "Malicious" + ? { + header: "This contract was flagged as malicious", + variant: WarningMessageVariant.highAlert, + message: + "Proceed with caution. Blockaid has flagged this transaction as malicious.", + } + : { + header: "This contract was flagged as suspicious", + variant: WarningMessageVariant.warning, + message: + "Proceed with caution. Blockaid has flagged this transaction as suspicious.", + }; + return ( + +
+

{t(details.message)}

+
+
+ ); +}; diff --git a/extension/src/popup/helpers/blockaid.ts b/extension/src/popup/helpers/blockaid.ts index c8ef368d2..91e6e801b 100644 --- a/extension/src/popup/helpers/blockaid.ts +++ b/extension/src/popup/helpers/blockaid.ts @@ -21,9 +21,11 @@ interface BlockAidScanSiteResult { // ... } -interface BlockAidScanTxResult { +export interface BlockAidScanTxResult { simulation: object; - validation: object; + validation: { + result_type: "Benign" | "Warning" | "Malicious"; + }; } export const useScanSite = () => { @@ -69,7 +71,7 @@ export const useScanSite = () => { }; export const useScanTx = () => { - const [data, setData] = useState({} as BlockAidScanTxResult); + const [data, setData] = useState(null as BlockAidScanTxResult | null); const [error, setError] = useState(null as string | null); const [isLoading, setLoading] = useState(true); @@ -83,7 +85,7 @@ export const useScanTx = () => { if (isCustomNetwork(networkDetails)) { setError("Scanning transactions is not supported on custom networks"); setLoading(false); - return; + return null; } const response = await fetchJson<{ data: BlockAidScanTxResult; @@ -97,6 +99,7 @@ export const useScanTx = () => { setData(response.data); emitMetric(METRIC_NAMES.blockaidTxScan, { response: response.data }); setLoading(false); + return response.data; } catch (err) { setError("Failed to scan transaction"); Sentry.captureException({ @@ -107,6 +110,7 @@ export const useScanTx = () => { }); setLoading(false); } + return null; }; return { @@ -117,6 +121,18 @@ export const useScanTx = () => { }; }; +interface ScanAssetResponseSuccess { + data: { + result_type: "Benign" | "Warning" | "Malicious" | "Spam"; + }; + error: null; +} +interface ScanAssetResponseError { + data: null; + error: string; +} +type ScanAssetResponse = ScanAssetResponseSuccess | ScanAssetResponseError; + export const scanAsset = async ( address: string, networkDetails: NetworkDetails, @@ -127,12 +143,9 @@ export const scanAsset = async ( return {}; } const res = await fetch(`${INDEXER_URL}/scan-asset?address=${address}`); - const response = (await res.json()) as { - data: BlockAidScanTxResult; - error: string | null; - }; + const response = (await res.json()) as ScanAssetResponse; - if (!res.ok) { + if (!res.ok || response.error) { Sentry.captureException(response.error || "Failed to scan asset"); } diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index a73471461..bdd63c12b 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -28,7 +28,7 @@ "Add New Address": "Add New Address", "Add new list": "Add new list", "Address": "Address", - "Addresses are uppercase and begin with letters “G“ or “M“": "Addresses are uppercase and begin with letters “G“ or “M“.", + "Addresses are uppercase and begin with letters “G“, “M“, or “C“": "Addresses are uppercase and begin with letters “G“, “M“, or “C“.", "Advanced settings": "Advanced settings", "Advanced Settings": "Advanced Settings", "Advanced settings are not recommended for new or unexperienced users": { @@ -243,9 +243,9 @@ "Insufficient Balance": "Insufficient Balance", "Insufficient Fee": "Insufficient Fee", "INSUFFICIENT FUNDS FOR FEE": "INSUFFICIENT FUNDS FOR FEE", + "invalid destination address": "invalid destination address", "invalid federation address": "invalid federation address", "Invalid Format Asset": "Invalid Format Asset", - "invalid public key": "invalid public key", "INVALID STELLAR ADDRESS": "INVALID STELLAR ADDRESS", "Invocation Type": "Invocation Type", "is requesting approval to sign a message": "is requesting approval to sign a message", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 706703bfb..4acf8d932 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -28,7 +28,7 @@ "Add New Address": "Add New Address", "Add new list": "Add new list", "Address": "Address", - "Addresses are uppercase and begin with letters “G“ or “M“": "Addresses are uppercase and begin with letters “G“ or “M“.", + "Addresses are uppercase and begin with letters “G“, “M“, or “C“": "Addresses are uppercase and begin with letters “G“, “M“, or “C“.", "Advanced settings": "Advanced settings", "Advanced Settings": "Advanced Settings", "Advanced settings are not recommended for new or unexperienced users": { @@ -243,9 +243,9 @@ "Insufficient Balance": "Insufficient Balance", "Insufficient Fee": "Insufficient Fee", "INSUFFICIENT FUNDS FOR FEE": "INSUFFICIENT FUNDS FOR FEE", + "invalid destination address": "invalid destination address", "invalid federation address": "invalid federation address", "Invalid Format Asset": "Invalid Format Asset", - "invalid public key": "invalid public key", "INVALID STELLAR ADDRESS": "INVALID STELLAR ADDRESS", "Invocation Type": "Invocation Type", "is requesting approval to sign a message": "is requesting approval to sign a message", diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index 9402abea1..9319a11f9 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -53,6 +53,7 @@ import { FirstTimeWarningMessage, FlaggedWarningMessage, SSLWarningMessage, + BlockaidMaliciousTxWarning, } from "popup/components/WarningMessages"; import { HardwareSign } from "popup/components/hardwareConnect/HardwareSign"; import { KeyIdenticon } from "popup/components/identicons/KeyIdenticon"; @@ -82,7 +83,7 @@ export const SignTransaction = () => { const isNonSSLEnabled = useSelector(isNonSSLEnabledSelector); const networkDetails = useSelector(settingsNetworkDetailsSelector); const { networkName, networkPassphrase } = networkDetails; - const { scanTx } = useScanTx(); + const { scanTx, isLoading: isLoadingScan, data: scanResult } = useScanTx(); const tx = getTransactionInfo(location.search); const { url } = parsedSearchParam(location.search); @@ -238,7 +239,7 @@ export const SignTransaction = () => { accountBalanceStatus !== ActionStatus.PENDING && accountBalanceStatus !== ActionStatus.IDLE; - if (!hasLoadedBalances) { + if (!hasLoadedBalances || isLoadingScan) { return ; } @@ -333,6 +334,11 @@ export const SignTransaction = () => { {!isDomainListedAllowed && !isSubmitDisabled ? ( ) : null} + {scanResult && scanResult.validation.result_type !== "Benign" && ( + + )} {renderTabBody()} ); From 786cbb50bca2dffa514b05f4a7834fbe263a7424 Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Wed, 28 Aug 2024 10:26:35 -0600 Subject: [PATCH 05/67] adds scan labels for all scan result types, tweaks styles --- .../src/popup/components/ModalInfo/index.tsx | 46 ++++++------- .../popup/components/ModalInfo/styles.scss | 10 --- .../components/WarningMessages/index.tsx | 66 +++++++++++++------ .../components/WarningMessages/styles.scss | 40 +++++++++-- .../src/popup/views/GrantAccess/index.tsx | 3 +- 5 files changed, 103 insertions(+), 62 deletions(-) diff --git a/extension/src/popup/components/ModalInfo/index.tsx b/extension/src/popup/components/ModalInfo/index.tsx index 46230bf5d..10168c2b0 100644 --- a/extension/src/popup/components/ModalInfo/index.tsx +++ b/extension/src/popup/components/ModalInfo/index.tsx @@ -1,9 +1,8 @@ import React from "react"; -import classNames from "classnames"; import { Card, Icon } from "@stellar/design-system"; import { PunycodedDomain } from "popup/components/PunycodedDomain"; -import { MaliciousDomainWarning } from "../WarningMessages"; +import { BlockAidSiteScanLabel } from "../WarningMessages"; import "./styles.scss"; @@ -11,34 +10,29 @@ interface ModalInfoProps { children: React.ReactNode; domain: string; subject: string; - variant?: "default" | "malicious"; + isMalicious: boolean; + scanStatus: "hit" | "miss"; } export const ModalInfo = ({ children, domain, subject, - variant = "default", -}: ModalInfoProps) => { - const cardClasses = classNames("ModalInfo--card", { - Malicious: variant === "malicious", - }); - return ( -
- - -
-
- -

Connection Request

-
+ isMalicious, + scanStatus, +}: ModalInfoProps) => ( +
+ + +
+
+ +

Connection Request

- {variant === "malicious" && ( - - )} -
{subject}
- {children} - -
- ); -}; +
+ +
{subject}
+ {children} + +
+); diff --git a/extension/src/popup/components/ModalInfo/styles.scss b/extension/src/popup/components/ModalInfo/styles.scss index 34ec65861..805c7552b 100644 --- a/extension/src/popup/components/ModalInfo/styles.scss +++ b/extension/src/popup/components/ModalInfo/styles.scss @@ -1,14 +1,4 @@ .ModalInfo { - &--card.Malicious { - .Card { - .PunycodedDomain { - .PunycodedDomain__domain { - color: var(--color-red-50); - } - } - } - } - &--connection-request { display: flex; justify-content: center; diff --git a/extension/src/popup/components/WarningMessages/index.tsx b/extension/src/popup/components/WarningMessages/index.tsx index f0d12c3d7..cce0d3d79 100644 --- a/extension/src/popup/components/WarningMessages/index.tsx +++ b/extension/src/popup/components/WarningMessages/index.tsx @@ -1078,34 +1078,62 @@ export const SSLWarningMessage = ({ url }: { url: string }) => { ); }; -export const MaliciousDomainWarning = ({ message }: { message: string }) => ( -
-
- +export const BlockAidMaliciousLabel = () => { + const { t } = useTranslation(); + return ( +
+
+ +
+

{t("This site was flagged as malicious")}

-

{message}

-
-); + ); +}; -export const BlockAidMissWarning = () => { +export const BlockAidBenignLabel = () => { const { t } = useTranslation(); + return ( +
+
+ +
+

{t("This site has been scanned and verified")}

+
+ ); +}; +export const BlockAidMissLabel = () => { + const { t } = useTranslation(); return ( - -
-

- {t( - "Proceed with caution. Blockaid is unable to provide a risk assesment for this domain at this time.", - )} -

+
+
+
- +

+ {t("Unable to scan site for malicious behavior")} +

+
); }; +export const BlockAidSiteScanLabel = ({ + status, + isMalicious, +}: { + status: "hit" | "miss"; + isMalicious: boolean; +}) => { + if (status === "miss") { + return ; + } + + if (isMalicious) { + return ; + } + + return ; +}; + export const BlockaidMaliciousTxWarning = ({ type, }: { diff --git a/extension/src/popup/components/WarningMessages/styles.scss b/extension/src/popup/components/WarningMessages/styles.scss index 93681a2a0..9dae36721 100644 --- a/extension/src/popup/components/WarningMessages/styles.scss +++ b/extension/src/popup/components/WarningMessages/styles.scss @@ -417,20 +417,16 @@ overflow-wrap: break-word; } -.MaliciousDomainWarning { +.ScanLabel { display: flex; - background-color: var(--color-red-20); - border: 1px solid #671e22; border-radius: 6px; margin-bottom: 10px; padding: 0.5rem; padding-left: 0.25rem; .Icon { + height: 16px; margin-right: 5px; - .WarningMessage__icon { - color: var(--color-red-60); - } } .Message { @@ -438,3 +434,35 @@ line-height: 1rem; } } + +.ScanMalicious { + background-color: var(--color-red-20); + border: 1px solid #671e22; + + .Icon { + .WarningMessage__icon { + color: var(--color-red-60); + } + } +} + +.ScanBenign { + background-color: var(--color-green-40); + border: 1px solid var(--color-green-50); + + .Icon { + .WarningMessage__icon { + color: var(--color-green-50); + } + } +} + +.ScanMiss { + border: 1px solid var(--color-grey-40); + + .Icon { + .WarningMessage__icon { + color: var(--color-grey-40); + } + } +} diff --git a/extension/src/popup/views/GrantAccess/index.tsx b/extension/src/popup/views/GrantAccess/index.tsx index b7ef68486..2e42c6d8a 100644 --- a/extension/src/popup/views/GrantAccess/index.tsx +++ b/extension/src/popup/views/GrantAccess/index.tsx @@ -58,7 +58,8 @@ export const GrantAccess = () => { ) : ( Date: Wed, 28 Aug 2024 10:27:29 -0600 Subject: [PATCH 06/67] Added translations --- extension/src/popup/locales/en/translation.json | 7 ++++--- extension/src/popup/locales/pt/translation.json | 7 ++++--- extension/src/popup/views/GrantAccess/index.tsx | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index bdd63c12b..d045ef3ef 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -368,9 +368,7 @@ "Preferences": "Preferences", "Price": "Price", "Privacy Policy": "Privacy Policy", - "Proceed with caution": { - " Blockaid is unable to provide a risk assesment for this domain at this time": "Proceed with caution. Blockaid is unable to provide a risk assesment for this domain at this time." - }, + "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. " @@ -493,6 +491,8 @@ "This invocation authorizes the following transfers, please review the invocation tree and confirm that you want to proceed": "This invocation authorizes the following transfers, please review the invocation tree and confirm that you want to proceed.", "This is a relatively new asset": "This is a relatively new asset.", "This new backup phrase will be used for your new accounts": "This new backup phrase will be used for your new accounts.", + "This site has been scanned and verified": "This site has been scanned and verified", + "This site was flagged as malicious": "This site was flagged as malicious", "This transaction could not be completed": "This transaction could not be completed.", "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.", @@ -515,6 +515,7 @@ "Unable to connect to": "Unable to connect to", "Unable to migrate": "Unable to migrate", "Unable to parse assets lists": "Unable to parse assets lists", + "Unable to scan site for malicious behavior": "Unable to scan site for malicious behavior", "Unable to search for assets": "Unable to search for assets", "Unsafe": "Unsafe", "Unsupported signing method": "Unsupported signing method", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 4acf8d932..bc8e1168e 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -368,9 +368,7 @@ "Preferences": "Preferences", "Price": "Price", "Privacy Policy": "Privacy Policy", - "Proceed with caution": { - " Blockaid is unable to provide a risk assesment for this domain at this time": "Proceed with caution. Blockaid is unable to provide a risk assesment for this domain at this time." - }, + "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. " @@ -493,6 +491,8 @@ "This invocation authorizes the following transfers, please review the invocation tree and confirm that you want to proceed": "This invocation authorizes the following transfers, please review the invocation tree and confirm that you want to proceed.", "This is a relatively new asset": "This is a relatively new asset.", "This new backup phrase will be used for your new accounts": "This new backup phrase will be used for your new accounts.", + "This site has been scanned and verified": "This site has been scanned and verified", + "This site was flagged as malicious": "This site was flagged as malicious", "This transaction could not be completed": "This transaction could not be completed.", "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.", @@ -515,6 +515,7 @@ "Unable to connect to": "Unable to connect to", "Unable to migrate": "Unable to migrate", "Unable to parse assets lists": "Unable to parse assets lists", + "Unable to scan site for malicious behavior": "Unable to scan site for malicious behavior", "Unable to search for assets": "Unable to search for assets", "Unsafe": "Unsafe", "Unsupported signing method": "Unsupported signing method", diff --git a/extension/src/popup/views/GrantAccess/index.tsx b/extension/src/popup/views/GrantAccess/index.tsx index 2e42c6d8a..96b55809a 100644 --- a/extension/src/popup/views/GrantAccess/index.tsx +++ b/extension/src/popup/views/GrantAccess/index.tsx @@ -84,7 +84,7 @@ export const GrantAccess = () => { + )} - )}
{" "}
-
+ ); diff --git a/extension/src/popup/components/WarningMessages/styles.scss b/extension/src/popup/components/WarningMessages/styles.scss index 9dae36721..f22b3c6ee 100644 --- a/extension/src/popup/components/WarningMessages/styles.scss +++ b/extension/src/popup/components/WarningMessages/styles.scss @@ -113,13 +113,25 @@ padding-bottom: 1rem; } + &__box { + display: flex; + background-color: var(--color-red-20); + border: 1px solid #671e22; + border-radius: 6px; + margin-bottom: 1.5rem; + padding: 0.5rem; + + &__icon { + margin: 0.25rem 0.375rem 0 0; + } + } + &__wrapper { z-index: calc(var(--z-index--scam-warning) + 1); height: 100%; bottom: 0; display: flex; flex-direction: column; - background: var(--color-gray-00); transition: bottom var(--dropdown-animation); border-top-right-radius: 1rem; border-top-left-radius: 1rem; @@ -134,13 +146,22 @@ } &__description { - font-size: 0.875rem; + font-size: 0.75rem; line-height: 1.375rem; - margin: 0.5rem 0 2rem 0; + margin-bottom: 0.375rem; } - &__bottom-content { - margin-top: auto; + &__list { + font-size: 0.75rem !important; + margin-left: 1.125rem !important; + } + + &__footer { + color: var(--color-gray-70); + display: flex; + font-size: 0.75rem; + gap: 0.25rem; + line-height: 1.125rem; } &__row { @@ -199,7 +220,6 @@ z-index: calc(var(--z-index--scam-warning) + 1); display: flex; flex-direction: column; - background: var(--color-gray-00); transition: bottom var(--dropdown-animation); border-top-right-radius: 1rem; border-top-left-radius: 1rem; diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index cf446e955..b8a6e4b66 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -40,6 +40,7 @@ export const AssetIcon = ({ isLPShare = false, isSorobanToken = false, icon, + isMalicious = false, }: { assetIcons: AssetIcons; code: string; @@ -48,6 +49,7 @@ export const AssetIcon = ({ isLPShare?: boolean; isSorobanToken?: boolean; icon?: string; + isMalicious?: boolean; }) => { /* We load asset icons in 2 ways: @@ -129,11 +131,13 @@ export const AssetIcon = ({ setIsLoading(false); }} /> + ) : ( // the image path wasn't found, show a default broken image icon
+
); }; @@ -152,9 +156,6 @@ export const AccountAssets = ({ const [assetIcons, setAssetIcons] = useState(inputAssetIcons); const networkDetails = useSelector(settingsNetworkDetailsSelector); const [hasIconFetchRetried, setHasIconFetchRetried] = useState(false); - const { assetDomains, blockedDomains } = useSelector( - transactionSubmissionSelector, - ); useEffect(() => { setAssetIcons(inputAssetIcons); @@ -237,8 +238,7 @@ export const AccountAssets = ({ issuer?.key as string, ); - const assetDomain = assetDomains[canonicalAsset]; - const isScamAsset = !!blockedDomains.domains[assetDomain]; + const isMalicious = rb.isMalicious || false; const bigTotal = new BigNumber(rb.total as string); const amountVal = rb.contractId @@ -263,9 +263,9 @@ export const AccountAssets = ({ issuerKey={issuer?.key} retryAssetIconFetch={retryAssetIconFetch} isLPShare={!!rb.liquidityPoolId} + isMalicious={isMalicious} /> {code} -
diff --git a/extension/src/popup/components/account/AccountAssets/styles.scss b/extension/src/popup/components/account/AccountAssets/styles.scss index 3f286e706..9c7b7c7a6 100644 --- a/extension/src/popup/components/account/AccountAssets/styles.scss +++ b/extension/src/popup/components/account/AccountAssets/styles.scss @@ -17,6 +17,7 @@ $loader-light-color: #444961; margin-right: 1rem; max-width: 2rem; max-height: 2rem; + position: relative; img { width: 100%; diff --git a/extension/src/popup/components/account/AssetDetail/index.tsx b/extension/src/popup/components/account/AssetDetail/index.tsx index f65235bc1..354e34f38 100644 --- a/extension/src/popup/components/account/AssetDetail/index.tsx +++ b/extension/src/popup/components/account/AssetDetail/index.tsx @@ -1,11 +1,12 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { BigNumber } from "bignumber.js"; import { useTranslation } from "react-i18next"; -import { IconButton, Icon, Notification } from "@stellar/design-system"; +import { IconButton, Icon } from "@stellar/design-system"; import { HorizonOperation, AssetType } from "@shared/api/types"; import { NetworkDetails } from "@shared/constants/stellar"; +import { stellarSdkServer } from "@shared/api/helpers/stellarSdkServer"; import { getAvailableBalance, getIsPayment, @@ -19,6 +20,7 @@ import { useAssetDomain } from "popup/helpers/useAssetDomain"; import { navigateTo } from "popup/helpers/navigate"; import { formatTokenAmount, isContractId } from "popup/helpers/soroban"; import { getAssetFromCanonical } from "helpers/stellar"; +import { checkForSuspiciousAsset } from "popup/helpers/checkForSuspiciousAsset"; import { ROUTES } from "popup/constants/routes"; import { PillButton } from "popup/basics/buttons/PillButton"; @@ -43,10 +45,11 @@ import { transactionSubmissionSelector, } from "popup/ducks/transactionSubmission"; import { AppDispatch } from "popup/App"; -import { useIsOwnedScamAsset } from "popup/helpers/useIsOwnedScamAsset"; import StellarLogo from "popup/assets/stellar-logo.png"; import { formatAmount } from "popup/helpers/formatters"; +import { useScanAsset } from "popup/helpers/blockaid"; import { Loading } from "popup/components/Loading"; +import { BlockaidAssetWarning } from "popup/components/WarningMessages"; import "./styles.scss"; @@ -70,18 +73,40 @@ export const AssetDetail = ({ subentryCount, }: AssetDetailProps) => { const dispatch: AppDispatch = useDispatch(); + const [isNewAsset, setIsNewAsset] = useState(false); const isNative = selectedAsset === "native"; const canonical = getAssetFromCanonical(selectedAsset); const isSorobanAsset = canonical.issuer && isSorobanIssuer(canonical.issuer); - const isOwnedScamAsset = useIsOwnedScamAsset( - canonical.code, - canonical.issuer, - ); const { accountBalances: balances } = useSelector( transactionSubmissionSelector, ); + const { scannedAsset } = useScanAsset( + `${canonical.code}-${canonical.issuer}`, + ); + const isMalicious = scannedAsset.result_type === "Malicious"; + + useEffect(() => { + const fetchSuspiciousAsset = async () => { + const server = stellarSdkServer( + networkDetails.networkUrl, + networkDetails.networkPassphrase, + ); + + const resp = await checkForSuspiciousAsset({ + code: canonical.code, + issuer: canonical.issuer, + domain: "", + server, + networkDetails, + }); + + setIsNewAsset(resp.isNewAsset); + }; + + fetchSuspiciousAsset(); + }, [canonical.code, canonical.issuer, networkDetails]); const balance = getRawBalance(accountBalances, selectedAsset)!; @@ -163,7 +188,9 @@ export const AssetDetail = ({ )}
{displayTotal} @@ -223,20 +250,12 @@ export const AssetDetail = ({ )}
- {isOwnedScamAsset && ( - -
-

- This asset was tagged as fraudulent by stellar.expert, a - reliable community-maintained directory. -

-

- Trading or sending this asset is not recommended. Projects - related to this asset may be fraudulent even if the creators - say otherwise. -

-
-
+ {isMalicious && ( + )}
diff --git a/extension/src/popup/components/account/AssetDetail/styles.scss b/extension/src/popup/components/account/AssetDetail/styles.scss index aba05a1e7..657cea652 100644 --- a/extension/src/popup/components/account/AssetDetail/styles.scss +++ b/extension/src/popup/components/account/AssetDetail/styles.scss @@ -53,6 +53,10 @@ line-height: 150%; margin: 2.5rem 0 0.5rem 0; text-align: center; + + &--isMalicious { + color: var(--color-red-70); + } } } diff --git a/extension/src/popup/components/account/ScamAssetIcon/index.tsx b/extension/src/popup/components/account/ScamAssetIcon/index.tsx index 57b9392aa..df80f65a2 100644 --- a/extension/src/popup/components/account/ScamAssetIcon/index.tsx +++ b/extension/src/popup/components/account/ScamAssetIcon/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import IconWarning from "popup/assets/icon-warning-red.svg"; +import IconWarning from "popup/assets/icon-warning-asset-blockaid.svg"; import "./styles.scss"; export const ScamAssetIcon = ({ isScamAsset }: { isScamAsset: boolean }) => diff --git a/extension/src/popup/components/account/ScamAssetIcon/styles.scss b/extension/src/popup/components/account/ScamAssetIcon/styles.scss index 7d48d55f3..eba06e844 100644 --- a/extension/src/popup/components/account/ScamAssetIcon/styles.scss +++ b/extension/src/popup/components/account/ScamAssetIcon/styles.scss @@ -1,5 +1,13 @@ .ScamAssetIcon { - margin-left: 0.25rem; display: flex; align-items: center; + background: var(--color-red-40); + border-radius: 1rem; + position: absolute; + bottom: 0; + right: 0; + padding: 0.25rem; + width: 50%; + height: 50%; + background-size: auto; } diff --git a/extension/src/popup/components/accountHistory/AssetNetworkInfo/index.tsx b/extension/src/popup/components/accountHistory/AssetNetworkInfo/index.tsx index 542e29375..fdaad5def 100644 --- a/extension/src/popup/components/accountHistory/AssetNetworkInfo/index.tsx +++ b/extension/src/popup/components/accountHistory/AssetNetworkInfo/index.tsx @@ -4,8 +4,6 @@ import { useSelector } from "react-redux"; import { getIconUrlFromIssuer } from "@shared/api/helpers/getIconUrlFromIssuer"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; -import { transactionSubmissionSelector } from "popup/ducks/transactionSubmission"; -import { ScamAssetIcon } from "popup/components/account/ScamAssetIcon"; import { CopyValue } from "popup/components/CopyValue"; import StellarLogo from "popup/assets/stellar-logo.png"; import { displaySorobanId, isSorobanIssuer } from "popup/helpers/account"; @@ -28,9 +26,7 @@ export const AssetNetworkInfo = ({ contractId, }: AssetNetworkInfoProps) => { const networkDetails = useSelector(settingsNetworkDetailsSelector); - const { blockedDomains } = useSelector(transactionSubmissionSelector); const [networkIconUrl, setNetworkIconUrl] = useState(""); - const isBlockedDomain = blockedDomains.domains[assetDomain]; useEffect(() => { const fetchIconUrl = async () => { @@ -55,9 +51,6 @@ export const AssetNetworkInfo = ({ }, [assetCode, assetIssuer, networkDetails]); const decideNetworkIcon = () => { - if (isBlockedDomain) { - return ; - } if (networkIconUrl || assetType === "native") { return Network icon; } diff --git a/extension/src/popup/components/accountHistory/TransactionDetail/index.tsx b/extension/src/popup/components/accountHistory/TransactionDetail/index.tsx index 0461b766b..b9e5e9472 100644 --- a/extension/src/popup/components/accountHistory/TransactionDetail/index.tsx +++ b/extension/src/popup/components/accountHistory/TransactionDetail/index.tsx @@ -13,6 +13,7 @@ import { emitMetric } from "helpers/metrics"; import { openTab } from "popup/helpers/navigate"; import { stroopToXlm } from "helpers/stellar"; import { useAssetDomain } from "popup/helpers/useAssetDomain"; +import { useScanAsset } from "popup/helpers/blockaid"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { METRIC_NAMES } from "popup/constants/metricsNames"; @@ -78,8 +79,10 @@ export const TransactionDetail = ({ const { assetDomain, error: assetError } = useAssetDomain({ assetIssuer, }); + const { scannedAsset } = useScanAsset(`${assetCode}-${assetIssuer}`); const networkDetails = useSelector(settingsNetworkDetailsSelector); const showContent = assetIssuer && !assetDomain && !assetError; + const isMalicious = scannedAsset.result_type === "Malicious"; return showContent ? ( @@ -92,7 +95,11 @@ export const TransactionDetail = ({
{isPayment ? ( -
+
{operationText} { const { token: { code, issuer }, contractId, + isMalicious, } = sortedBalances[i]; // If we are in the swap flow and the asset has decimals (is a token), we skip it if Soroswap is not enabled @@ -99,6 +100,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { ], domain, contract: contractId, + isMalicious, }); // include native asset for asset dropdown selection } else if (!isManagingAssets) { @@ -107,6 +109,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { issuer: "", image: "", domain: "", + isMalicious: false, }); } } diff --git a/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx index b86fc2e07..33f3b839d 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx @@ -91,9 +91,7 @@ export const ManageAssetRowButton = ({ const [isTrustlineErrorShowing, setIsTrustlineErrorShowing] = useState(false); const [isSigningWithHardwareWallet, setIsSigningWithHardwareWallet] = useState(false); - const { blockedDomains, submitStatus } = useSelector( - transactionSubmissionSelector, - ); + const { submitStatus } = useSelector(transactionSubmissionSelector); const walletType = useSelector(hardwareWalletTypeSelector); const networkDetails = useSelector(settingsNetworkDetailsSelector); const publicKey = useSelector(publicKeySelector); @@ -105,8 +103,6 @@ export const ManageAssetRowButton = ({ networkDetails.networkPassphrase, ); - const isBlockedDomain = (d: string) => blockedDomains.domains[d]; - const handleBackgroundClick = () => { setRowButtonShowing(""); }; @@ -211,14 +207,18 @@ export const ManageAssetRowButton = ({ networkDetails, }); - await scanAsset( + const scannedAsset = await scanAsset( `${assetRowData.code}-${assetRowData.issuer}`, networkDetails, ); - if (isBlockedDomain(assetRowData.domain) && !isTrustlineActive) { + if (scannedAsset.result_type === "Malicious" && !isTrustlineActive) { setShowBlockedDomainWarning(true); - setSuspiciousAssetData(assetRowData); + setSuspiciousAssetData({ + ...assetRowData, + blockaidWarning: scannedAsset.result_type, + isNewAsset: resp.isNewAsset, + }); } else if ( !isTrustlineActive && (resp.isInvalidDomain || resp.isRevocable || resp.isNewAsset) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index 145a2c49a..39fb0552d 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -30,7 +30,6 @@ import { NewAssetWarning, TokenWarning, } from "popup/components/WarningMessages"; -import { ScamAssetIcon } from "popup/components/account/ScamAssetIcon"; import { ManageAssetRowButton } from "../ManageAssetRowButton"; @@ -40,6 +39,7 @@ export type ManageAssetCurrency = StellarToml.Api.Currency & { domain: string; contract?: string; icon?: string; + isMalicious?: boolean; }; export interface NewAssetFlags { @@ -63,6 +63,8 @@ interface SuspiciousAssetData { issuer: string; image: string; isVerifiedToken?: boolean; + blockaidWarning: string; + isNewAsset: boolean; } export const ManageAssetRows = ({ @@ -99,6 +101,8 @@ export const ManageAssetRows = ({ issuer: "", image: "", isVerifiedToken: false, + blockaidWarning: "", + isNewAsset: false, } as SuspiciousAssetData); const [handleAddToken, setHandleAddToken] = useState( null as null | (() => () => Promise), @@ -126,10 +130,12 @@ export const ManageAssetRows = ({ )} {showBlockedDomainWarning && ( { setShowBlockedDomainWarning(false); }} @@ -170,6 +176,7 @@ export const ManageAssetRows = ({ issuer = "", name = "", contract = "", + isMalicious, }) => { if (!accountBalances.balances) { return null; @@ -194,6 +201,7 @@ export const ManageAssetRows = ({ image={image} domain={domain} name={name} + isMalicious={isMalicious} /> { - const { blockedDomains } = useSelector(transactionSubmissionSelector); const canonicalAsset = getCanonicalFromAsset(code, issuer); - const isScamAsset = !!blockedDomains.domains[domain]; const assetCode = name || code; const truncatedAssetCode = assetCode.length > 20 ? truncateString(assetCode) : assetCode; @@ -260,11 +268,11 @@ export const ManageAssetRow = ({ assetIcons={code !== "XLM" ? { [canonicalAsset]: image } : {}} code={code} issuerKey={issuer} + isMalicious={isMalicious} />
{truncatedAssetCode} -
{ const { accountBalances: { balances = {} }, assetSelect, - blockedDomains, soroswapTokens, transactionData, } = useSelector(transactionSubmissionSelector); @@ -75,8 +73,15 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => {
{assetRows.map( - ({ code = "", domain, image = "", issuer = "", icon }) => { - const isScamAsset = !!blockedDomains.domains[domain]; + ({ + code = "", + domain, + image = "", + issuer = "", + icon, + isMalicious, + }) => { + const isScamAsset = isMalicious || false; const isContract = isContractId(issuer); const canonical = getCanonicalFromAsset(code, issuer); let isSoroswap = false; @@ -118,11 +123,11 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => { code={code} issuerKey={issuer} icon={icon} + isMalicious={isScamAsset} />
{code} -
{formatDomain(domain)} diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx index aa531c5e5..47cb31eb9 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx @@ -6,7 +6,6 @@ import { ROUTES } from "popup/constants/routes"; import { navigateTo } from "popup/helpers/navigate"; import { isMainnet, isTestnet } from "helpers/stellar"; import { AssetIcon } from "popup/components/account/AccountAssets"; -import { ScamAssetIcon } from "popup/components/account/ScamAssetIcon"; import { UnverifiedTokenNotification } from "popup/components/WarningMessages"; import { transactionSubmissionSelector, @@ -16,7 +15,6 @@ import { } from "popup/ducks/transactionSubmission"; import { isContractId } from "popup/helpers/soroban"; import { useIsSwap } from "popup/helpers/useIsSwap"; -import { useIsOwnedScamAsset } from "popup/helpers/useIsOwnedScamAsset"; import { settingsSelector } from "popup/ducks/settings"; import { getVerifiedTokens } from "popup/helpers/searchAsset"; @@ -25,14 +23,15 @@ import "./styles.scss"; export const AssetSelect = ({ assetCode, issuerKey, + isMalicious, }: { assetCode: string; issuerKey: string; + isMalicious: boolean; }) => { const dispatch = useDispatch(); const { assetIcons } = useSelector(transactionSubmissionSelector); const { networkDetails, assetsLists } = useSelector(settingsSelector); - const isOwnedScamAsset = useIsOwnedScamAsset(assetCode, issuerKey); const [isUnverifiedToken, setIsUnverifiedToken] = useState(false); useEffect(() => { @@ -83,9 +82,9 @@ export const AssetSelect = ({ assetIcons={assetIcons} code={assetCode} issuerKey={issuerKey} + isMalicious={isMalicious} /> {assetCode} -
@@ -102,17 +101,18 @@ export const PathPayAssetSelect = ({ issuerKey, balance, icon, + isMalicious, }: { source: boolean; assetCode: string; issuerKey: string; balance: string; icon: string; + isMalicious: boolean; }) => { const dispatch = useDispatch(); const { assetIcons } = useSelector(transactionSubmissionSelector); const isSwap = useIsSwap(); - const isOwnedScamAsset = useIsOwnedScamAsset(assetCode, issuerKey); const handleSelectAsset = () => { dispatch( @@ -150,6 +150,7 @@ export const PathPayAssetSelect = ({ code={assetCode} issuerKey={issuerKey} icon={icon} + isMalicious={isMalicious} /> {truncateLongAssetCode(assetCode)} {" "} -
diff --git a/extension/src/popup/components/sendPayment/SendAmount/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/index.tsx index f0f4e7846..f6863cbab 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/index.tsx @@ -122,7 +122,6 @@ export const SendAmount = ({ destinationBalances, transactionData, assetDomains, - blockedDomains, assetIcons, soroswapTokens, } = useSelector(transactionSubmissionSelector); @@ -205,11 +204,17 @@ export const SendAmount = ({ }) => { dispatch(saveAmount(cleanAmount(values.amount))); dispatch(saveAsset(values.asset)); + // eslint-disable-next-line @typescript-eslint/naming-convention + let isDestAssetScam = false; + if (values.destinationAsset) { dispatch(saveDestinationAsset(values.destinationAsset)); + isDestAssetScam = + accountBalances.balances?.[destinationAsset].isMalicious || false; } // check for scam asset - if (blockedDomains.domains[assetDomains[values.asset]]) { + const isSourceAssetScam = accountBalances.balances?.[asset].isMalicious; + if (isSourceAssetScam) { setShowBlockedDomainWarning(true); setSuspiciousAssetData({ code: getAssetFromCanonical(values.asset).code, @@ -217,7 +222,7 @@ export const SendAmount = ({ domain: assetDomains[values.asset], image: assetIcons[values.asset], }); - } else if (blockedDomains.domains[assetDomains[values.destinationAsset]]) { + } else if (isDestAssetScam) { setShowBlockedDomainWarning(true); setSuspiciousAssetData({ code: getAssetFromCanonical(values.destinationAsset).code, @@ -425,12 +430,14 @@ export const SendAmount = ({ {showBlockedDomainWarning && ( setShowBlockedDomainWarning(false)} onContinue={() => navigateTo(next)} + blockaidWarning="Malicious" + isNewAsset={false} /> )} @@ -568,6 +575,9 @@ export const SendAmount = ({ )} {showSourceAndDestAsset && ( @@ -578,6 +588,10 @@ export const SendAmount = ({ issuerKey={parsedSourceAsset.issuer} balance={formik.values.amount} icon="" + isMalicious={ + accountBalances.balances?.[asset].isMalicious || + false + } /> )} diff --git a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx index 368b013e3..ada8411fd 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/SubmitResult/index.tsx @@ -32,29 +32,35 @@ import { View } from "popup/basics/layout/View"; import { AssetIcon } from "popup/components/account/AccountAssets"; import { TrustlineError } from "popup/components/manageAssets/TrustlineError"; import IconFail from "popup/assets/icon-fail.svg"; - -import "./styles.scss"; import { emitMetric } from "helpers/metrics"; import { METRIC_NAMES } from "popup/constants/metricsNames"; import { formatAmount } from "popup/helpers/formatters"; +import "./styles.scss"; + const SwapAssetsIcon = ({ sourceCanon, destCanon, assetIcons, + isSourceMalicious, + isDestMalicious, }: { sourceCanon: string; destCanon: string; assetIcons: AssetIcons; + isSourceMalicious: boolean; + isDestMalicious: boolean; }) => { const source = getAssetFromCanonical(sourceCanon); const dest = getAssetFromCanonical(destCanon); + return (
{source.code} @@ -62,6 +68,7 @@ const SwapAssetsIcon = ({ assetIcons={assetIcons} code={dest.code} issuerKey={dest.issuer} + isMalicious={isDestMalicious} /> {dest.code}
@@ -96,6 +103,11 @@ export const SubmitSuccess = ({ viewDetails }: { viewDetails: () => void }) => { networkDetails.networkPassphrase, ); const isHardwareWallet = !!useSelector(hardwareWalletTypeSelector); + const isSourceAssetMalicious = + accountBalances.balances?.[asset].isMalicious || false; + const isDestAssetMalicious = destinationAsset + ? accountBalances.balances?.[destinationAsset].isMalicious || false + : false; const removeTrustline = async (assetCode: string, assetIssuer: string) => { const changeParams = { limit: "0" }; @@ -213,6 +225,8 @@ export const SubmitSuccess = ({ viewDetails }: { viewDetails: () => void }) => { sourceCanon={asset} destCanon={destinationAsset} assetIcons={assetIcons} + isSourceMalicious={isSourceAssetMalicious} + isDestMalicious={isDestAssetMalicious} /> ) : ( { const sourceAsset = getAssetFromCanonical(sourceCanon); const destAsset = getAssetFromCanonical(destCanon); - const isSourceAssetScam = useIsOwnedScamAsset( - sourceAsset.code, - sourceAsset.issuer, - ); - const isDestAssetScam = useIsOwnedScamAsset(destAsset.code, destAsset.issuer); - return (
@@ -103,9 +100,9 @@ const TwoAssetCard = ({ assetIcons={sourceAssetIcons} code={sourceAsset.code} issuerKey={sourceAsset.issuer} + isMalicious={isSourceAssetMalicious} /> {sourceAsset.code} -
{destAsset.code} -
void }) => { const dispatch: AppDispatch = useDispatch(); const submission = useSelector(transactionSubmissionSelector); const { + accountBalances, destinationBalances, transactionData: { destination, @@ -245,13 +243,21 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { matchingBlockedTags.some( (tag) => tag === TRANSACTION_WARNING.memoRequired && !memo, ); - const isMalicious = - isValidatingSafety && - matchingBlockedTags.some((tag) => tag === TRANSACTION_WARNING.malicious); + + const isAssetMalicious = + accountBalances.balances?.[asset].isMalicious || false; + const isTxMalicious = (isValidatingSafety && isAssetMalicious) || false; const isUnsafe = isValidatingSafety && matchingBlockedTags.some((tag) => tag === TRANSACTION_WARNING.unsafe); - const isSubmitDisabled = isMemoRequired || isMalicious || isUnsafe; + const isSubmitDisabled = isMemoRequired || isTxMalicious || isUnsafe; + + const destAssetToScan = destinationAsset + ? `${destAsset.code}-${destAsset.issuer}` + : ""; + + const { scannedAsset: scannedDestAsset } = useScanAsset(destAssetToScan); + const isDestAssetMalicious = scannedDestAsset.result_type === "Malicious"; // load destination asset icons useEffect(() => { @@ -482,6 +488,7 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { code: sourceAsset.code, }, total: amount || "0", + isMalicious: isAssetMalicious, }, ]} /> @@ -497,6 +504,8 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { destAssetIcons={destAssetIcons} destCanon={destinationAsset || "native"} destAmount={destinationAmount} + isSourceAssetMalicious={isAssetMalicious || false} + isDestAssetMalicious={isDestAssetMalicious} /> )} @@ -582,9 +591,10 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { )} {submission.submitStatus === ActionStatus.IDLE && ( )} diff --git a/extension/src/popup/helpers/blockaid.ts b/extension/src/popup/helpers/blockaid.ts index b0b5b5271..c4c57a1f2 100644 --- a/extension/src/popup/helpers/blockaid.ts +++ b/extension/src/popup/helpers/blockaid.ts @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import * as Sentry from "@sentry/browser"; +import { useSelector } from "react-redux"; import { INDEXER_URL } from "@shared/constants/mercury"; import { NetworkDetails } from "@shared/constants/stellar"; @@ -7,6 +8,7 @@ import { isCustomNetwork } from "@shared/helpers/stellar"; import { isMainnet } from "helpers/stellar"; import { emitMetric } from "helpers/metrics"; import { METRIC_NAMES } from "popup/constants/metricsNames"; +import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { fetchJson } from "./fetch"; export interface BlockAidScanSiteResult { @@ -121,10 +123,12 @@ export const useScanTx = () => { }; }; +interface BlockAidScanAssetResult { + result_type: "Benign" | "Warning" | "Malicious" | "Spam"; +} + interface ScanAssetResponseSuccess { - data: { - result_type: "Benign" | "Warning" | "Malicious" | "Spam"; - }; + data: BlockAidScanAssetResult; error: null; } interface ScanAssetResponseError { @@ -139,8 +143,8 @@ export const scanAsset = async ( ) => { try { if (!isMainnet(networkDetails)) { - console.error("Scanning assets is only supported on Mainnet"); - return {}; + /* Scanning assets is only supported on Mainnet */ + return {} as BlockAidScanAssetResult; } const res = await fetch(`${INDEXER_URL}/scan-asset?address=${address}`); const response = (await res.json()) as ScanAssetResponse; @@ -150,10 +154,35 @@ export const scanAsset = async ( } emitMetric(METRIC_NAMES.blockaidAssetScan, { response: response.data }); + if (!response.data) { + return {} as BlockAidScanAssetResult; + } return response.data; } catch (err) { console.error("Failed to scan asset"); Sentry.captureException(err); } - return {}; + return {} as BlockAidScanAssetResult; +}; + +export const useScanAsset = (address: string) => { + const networkDetails = useSelector(settingsNetworkDetailsSelector); + const [scannedAssetStatus, setScannedAssetStatus] = useState( + {} as BlockAidScanAssetResult, + ); + + useEffect(() => { + const fetchScanAssetStatus = async () => { + const scannedAsset = await scanAsset(address, networkDetails); + setScannedAssetStatus(scannedAsset); + }; + + if (address) { + fetchScanAssetStatus(); + } + }, [networkDetails, address]); + + return { + scannedAsset: scannedAssetStatus, + }; }; diff --git a/extension/src/popup/helpers/checkForSuspiciousAsset.ts b/extension/src/popup/helpers/checkForSuspiciousAsset.ts index f43a59949..0cdefee40 100644 --- a/extension/src/popup/helpers/checkForSuspiciousAsset.ts +++ b/extension/src/popup/helpers/checkForSuspiciousAsset.ts @@ -47,20 +47,23 @@ export const checkForSuspiciousAsset = async ({ // check domain let isInvalidDomain = false; - try { - const resp = await StellarToml.Resolver.resolve(domain); - let found = false; - (resp?.CURRENCIES || []).forEach( - (c: { code?: string; issuer?: string }) => { - if (c.code === code && c.issuer === issuer) { - found = true; - } - }, - ); - isInvalidDomain = !found; - } catch (e) { - console.error(e); - isInvalidDomain = true; + + if (domain) { + try { + const resp = await StellarToml.Resolver.resolve(domain); + let found = false; + (resp?.CURRENCIES || []).forEach( + (c: { code?: string; issuer?: string }) => { + if (c.code === code && c.issuer === issuer) { + found = true; + } + }, + ); + isInvalidDomain = !found; + } catch (e) { + console.error(e); + isInvalidDomain = true; + } } return { isRevocable, isNewAsset, isInvalidDomain }; diff --git a/extension/src/popup/helpers/useIsOwnedScamAsset.ts b/extension/src/popup/helpers/useIsOwnedScamAsset.ts deleted file mode 100644 index 17f27be55..000000000 --- a/extension/src/popup/helpers/useIsOwnedScamAsset.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSelector } from "react-redux"; -import { getCanonicalFromAsset } from "helpers/stellar"; -import { transactionSubmissionSelector } from "popup/ducks/transactionSubmission"; - -export const useIsOwnedScamAsset = (code: string, issuer: string) => { - const { assetDomains, blockedDomains } = useSelector( - transactionSubmissionSelector, - ); - - const canonicalAsset = getCanonicalFromAsset(code, issuer); - const assetDomain = assetDomains[canonicalAsset]; - return !!blockedDomains.domains[assetDomain]; -}; diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index d045ef3ef..4324a19a4 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -1,5 +1,4 @@ { - "{{srcAssetCode}} for {{destAssetCode}}": "{{srcAssetCode}} for {{destAssetCode}}", "* All Stellar accounts must maintain a minimum balance of lumens": "* All Stellar accounts must maintain a minimum balance of lumens.", "A destination account requires the use of the memo field which is not present in the transaction you’re about to sign": { " Freighter automatically disabled the option to sign this transaction": "A destination account requires the use of the memo field which is not present in the transaction you’re about to sign. Freighter automatically disabled the option to sign this transaction." @@ -16,9 +15,9 @@ "Add": "Add", "Add address": "Add address", "Add an asset": "Add an asset", - "Add anyway": "Add anyway", "Add asset": "Add asset", "Add Asset Trustline": "Add Asset Trustline", + "Add Asset trustlinet": "Add Asset trustlinet", "Add by address": "Add by address", "Add custom network": "Add custom network", "Add Custom Network": "Add Custom Network", @@ -51,8 +50,6 @@ "Allowed slippage": "Allowed slippage", "Always check the domain of websites you’re using Freighter with": "Always check the domain of websites you’re using Freighter with", "Amount": "Amount", - "An account you’re interacting with is tagged as malicious on": "An account you’re interacting with is tagged as malicious on", - "An account you’re interacting with is tagged as unsafe on": "An account you’re interacting with is tagged as unsafe on", "An error occurred": "An error occurred", "Anonymous data sharing": "Anonymous data sharing", "Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place": "Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place.", @@ -91,7 +88,6 @@ "Before You Add This Asset": "Before You Add This Asset", "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", "Bump To": "Bump To", "Buy Amount": "Buy Amount", "Buying": "Buying", @@ -119,6 +115,7 @@ "Connect device to computer": "Connect device to computer", "Connect to domain without SSL certificate": "Connect to domain without SSL certificate", "CONNECTION ERROR": "CONNECTION ERROR", + "Connection Request": "Connection Request", "Continue": "Continue", "Contract Address": "Contract Address", "Contract ID": "Contract ID", @@ -128,7 +125,6 @@ "Copy address": "Copy address", "Create a new Stellar address": "Create a new Stellar address", "Create a password": "Create a password", - "Create Account": "Create Account", "Create Wallet": "Create Wallet", "Custom": "Custom", "data entries": "data entries", @@ -145,14 +141,11 @@ "Detecting": "Detecting", "Different source and destination assets": "Different source and destination assets", "digits after the decimal allowed": "digits after the decimal allowed", - "directory": "directory", - "disable": "disable", "Disabled": "Disabled", "Done": "Done", "Double check the domain name": { " If it is incorrect in any way, do not share your public key and contact the site administrator via a verified email or social media account to confirm that this domain is correct": "Double check the domain name. If it is incorrect in any way, do not share your public key and contact the site administrator via a verified email or social media account to confirm that this domain is correct." }, - "enable": "enable", "Enable Blind Signing on Ledger": "Enable Blind Signing on Ledger", "Enable this list": "Enable this list", "Enabled": "Enabled", @@ -184,11 +177,7 @@ }, "Fetch list information": "Fetch list information", "Finish": "Finish", - "For your safety, signing this transaction is disabled": "For your safety, signing this transaction is disabled", "For your security, we'll check if you got it right in the next step": "For your security, we'll check if you got it right in the next step.", - "Freighter automatically blocked this asset": { - " Projects related to this asset may be fraudulent even if the creators say otherwise": "Freighter automatically blocked this asset. Projects related to this asset may be fraudulent even if the creators say otherwise." - }, "Freighter can’t recover your imported secret key using your recovery phrase": { " Storing your secret key is your responsibility": { " Freighter will never ask for your secret key outside of the extension": "Freighter can’t recover your imported secret key using your recovery phrase. Storing your secret key is your responsibility. Freighter will never ask for your secret key outside of the extension." @@ -226,6 +215,7 @@ "I’m going to need a seed phrase": "I’m going to need a seed phrase", "I’m new!": "I’m new!", "I’ve done this before": "I’ve done this before", + "Identified as a scam": "Identified as a scam", "If you believe you have interacted with this domain before, it is possible that scammers have copied the original site and/or made small changes to the domain name, and that this site is a scam": "If you believe you have interacted with this domain before, it is possible that scammers have copied the original site and/or made small changes to the domain name, and that this site is a scam.", "If you delete this list, you will have to re-add it manually": "If you delete this list, you will have to re-add it manually.", "Import": "Import", @@ -307,7 +297,6 @@ "Min Price": "Min Price", "Minimum Received": "Minimum Received", "Minimum resource fee": "Minimum resource fee", - "Minted": "Minted", "Multiple assets": "Multiple assets", "Multiple assets have a similar code, please check the domain before adding": "Multiple assets have a similar code, please check the domain before adding.", "must be below": "must be below", @@ -359,10 +348,10 @@ " This can help you identify fraudulent assets": "Please double-check its information and characteristics. This can help you identify fraudulent assets." }, "Please note the following message": "Please note the following message", - "Please proceed with caution": "Please proceed with caution.", "Please select a different network before removing it": "Please select a different network before removing it.", "Please select each word in the same order you have": "Please select each word in the same order you have", "powered by": "powered by", + "Powered by ": "Powered by ", "Pre Auth Transaction": "Pre Auth Transaction", "Predicate": "Predicate", "Preferences": "Preferences", @@ -370,12 +359,8 @@ "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. " - }, "Read before importing your key": "Read before importing your key", "Ready to migrate": "Ready to migrate", - "Received": "Received", "RECEIVED": "RECEIVED", "RECENT": "RECENT", "Recipient Stellar address": "Recipient Stellar address", @@ -420,9 +405,9 @@ "SET MAX": "SET MAX", "Set recommended": "Set recommended", "Settings": "Settings", - "Settings > Preferences": "Settings > Preferences", "Show recovery phrase": "Show recovery phrase", "Sign": "Sign", + "Sign anyway": "Sign anyway", "Sign Transaction": "Sign Transaction", "Signed Payload": "Signed Payload", "Signer": "Signer", @@ -449,7 +434,6 @@ "Swap failed": "Swap failed", "swapped": "swapped", "Swapped": "Swapped", - "Swapped {{srcAssetCode}} for {{destAssetCode}}": "Swapped {{srcAssetCode}} for {{destAssetCode}}", "Switch to this network": "Switch to this network", "Terms of Service": "Terms of Service", "Terms of Use": "Terms of Use", @@ -470,7 +454,6 @@ } }, "them noted to confirm you got them right": "them noted to confirm you got them right", - "this alert by going to": "this alert by going to", "This asset has a balance": "This asset has a balance", "This asset has buying liabilities": "This asset has buying liabilities", "This asset is not part of an asset list": { @@ -481,9 +464,6 @@ } }, "This asset is part of the asset lists": "This asset is part of the asset lists", - "This asset was tagged as fraudulent by stellar": { - "expert, a reliable community-maintained directory": "This asset was tagged as fraudulent by stellar.expert, a reliable community-maintained directory." - }, "This authorization uses the source account's credentials, so you are implicitly authorizing this when you sign the transaction": "This authorization uses the source account's credentials, so you are implicitly authorizing this when you sign the transaction.", "This can be used to sign arbitrary transaction hashes without having to decode them first": { " Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust": "This can be used to sign arbitrary transaction hashes without having to decode them first. Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust." @@ -493,6 +473,9 @@ "This new backup phrase will be used for your new accounts": "This new backup phrase will be used for your new accounts.", "This site has been scanned and verified": "This site has been scanned and verified", "This site was flagged as malicious": "This site was flagged as malicious", + "This token was flagged as malicious by Blockaid": { + " Interacting with this token may result in loss of funds and is not recommended for the following reasons": "This token was flagged as malicious by Blockaid. Interacting with this token may result in loss of funds and is not recommended for the following reasons" + }, "This transaction could not be completed": "This transaction could not be completed.", "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.", @@ -507,6 +490,7 @@ "Transaction": "Transaction", "Transaction failed": "Transaction failed", "Transaction fee": "Transaction fee", + "Transaction Request": "Transaction Request", "Transaction timeout": "Transaction timeout", "Transactions": "Transactions", "trustlines": "trustlines", @@ -546,7 +530,6 @@ "You are interacting with data that may be using untested and changing schemas": { " Proceed at your own risk": "You are interacting with data that may be using untested and changing schemas. Proceed at your own risk." }, - "You can": "You can", "You can choose to merge your current account into the new accounts after the migration, which will effectively destroy your current account": { " Merging is optional and will allow you to send your current account’s funding lumens to the new accounts": "You can choose to merge your current account into the new accounts after the migration, which will effectively destroy your current account. Merging is optional and will allow you to send your current account’s funding lumens to the new accounts." }, diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index bc8e1168e..33645a240 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -1,5 +1,4 @@ { - "{{srcAssetCode}} for {{destAssetCode}}": "{{srcAssetCode}} for {{destAssetCode}}", "* All Stellar accounts must maintain a minimum balance of lumens": "* All Stellar accounts must maintain a minimum balance of lumens.", "A destination account requires the use of the memo field which is not present in the transaction you’re about to sign": { " Freighter automatically disabled the option to sign this transaction": "A destination account requires the use of the memo field which is not present in the transaction you’re about to sign. Freighter automatically disabled the option to sign this transaction." @@ -16,9 +15,9 @@ "Add": "Add", "Add address": "Add address", "Add an asset": "Add an asset", - "Add anyway": "Add anyway", "Add asset": "Add asset", "Add Asset Trustline": "Add Asset Trustline", + "Add Asset trustlinet": "Add Asset trustlinet", "Add by address": "Add by address", "Add custom network": "Add custom network", "Add Custom Network": "Add Custom Network", @@ -51,8 +50,6 @@ "Allowed slippage": "Allowed slippage", "Always check the domain of websites you’re using Freighter with": "Always check the domain of websites you’re using Freighter with", "Amount": "Amount", - "An account you’re interacting with is tagged as malicious on": "An account you’re interacting with is tagged as malicious on", - "An account you’re interacting with is tagged as unsafe on": "An account you’re interacting with is tagged as unsafe on", "An error occurred": "An error occurred", "Anonymous data sharing": "Anonymous data sharing", "Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place": "Anyone who has access to this phrase has access to your account and to the funds in it, so save it in a safe and secure place.", @@ -91,7 +88,6 @@ "Before You Add This Asset": "Before You Add This Asset", "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", "Bump To": "Bump To", "Buy Amount": "Buy Amount", "Buying": "Buying", @@ -119,6 +115,7 @@ "Connect device to computer": "Connect device to computer", "Connect to domain without SSL certificate": "Connect to domain without SSL certificate", "CONNECTION ERROR": "CONNECTION ERROR", + "Connection Request": "Connection Request", "Continue": "Continue", "Contract Address": "Contract Address", "Contract ID": "Contract ID", @@ -128,7 +125,6 @@ "Copy address": "Copy address", "Create a new Stellar address": "Create a new Stellar address", "Create a password": "Create a password", - "Create Account": "Create Account", "Create Wallet": "Create Wallet", "Custom": "Custom", "data entries": "data entries", @@ -145,14 +141,11 @@ "Detecting": "Detecting", "Different source and destination assets": "Different source and destination assets", "digits after the decimal allowed": "digits after the decimal allowed", - "directory": "directory", - "disable": "disable", "Disabled": "Disabled", "Done": "Done", "Double check the domain name": { " If it is incorrect in any way, do not share your public key and contact the site administrator via a verified email or social media account to confirm that this domain is correct": "Double check the domain name. If it is incorrect in any way, do not share your public key and contact the site administrator via a verified email or social media account to confirm that this domain is correct." }, - "enable": "enable", "Enable Blind Signing on Ledger": "Enable Blind Signing on Ledger", "Enable this list": "Enable this list", "Enabled": "Enabled", @@ -184,11 +177,7 @@ }, "Fetch list information": "Fetch list information", "Finish": "Finish", - "For your safety, signing this transaction is disabled": "For your safety, signing this transaction is disabled", "For your security, we'll check if you got it right in the next step": "For your security, we'll check if you got it right in the next step.", - "Freighter automatically blocked this asset": { - " Projects related to this asset may be fraudulent even if the creators say otherwise": "Freighter automatically blocked this asset. Projects related to this asset may be fraudulent even if the creators say otherwise." - }, "Freighter can’t recover your imported secret key using your recovery phrase": { " Storing your secret key is your responsibility": { " Freighter will never ask for your secret key outside of the extension": "Freighter can’t recover your imported secret key using your recovery phrase. Storing your secret key is your responsibility. Freighter will never ask for your secret key outside of the extension." @@ -226,6 +215,7 @@ "I’m going to need a seed phrase": "I’m going to need a seed phrase", "I’m new!": "I’m new!", "I’ve done this before": "I’ve done this before", + "Identified as a scam": "Identified as a scam", "If you believe you have interacted with this domain before, it is possible that scammers have copied the original site and/or made small changes to the domain name, and that this site is a scam": "If you believe you have interacted with this domain before, it is possible that scammers have copied the original site and/or made small changes to the domain name, and that this site is a scam.", "If you delete this list, you will have to re-add it manually": "If you delete this list, you will have to re-add it manually.", "Import": "Import", @@ -307,7 +297,6 @@ "Min Price": "Min Price", "Minimum Received": "Minimum Received", "Minimum resource fee": "Minimum resource fee", - "Minted": "Minted", "Multiple assets": "Multiple assets", "Multiple assets have a similar code, please check the domain before adding": "Multiple assets have a similar code, please check the domain before adding.", "must be below": "must be below", @@ -359,10 +348,10 @@ " This can help you identify fraudulent assets": "Please double-check its information and characteristics. This can help you identify fraudulent assets." }, "Please note the following message": "Please note the following message", - "Please proceed with caution": "Please proceed with caution.", "Please select a different network before removing it": "Please select a different network before removing it.", "Please select each word in the same order you have": "Please select each word in the same order you have", "powered by": "powered by", + "Powered by ": "Powered by ", "Pre Auth Transaction": "Pre Auth Transaction", "Predicate": "Predicate", "Preferences": "Preferences", @@ -370,12 +359,8 @@ "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. " - }, "Read before importing your key": "Read before importing your key", "Ready to migrate": "Ready to migrate", - "Received": "Received", "RECEIVED": "RECEIVED", "RECENT": "RECENT", "Recipient Stellar address": "Recipient Stellar address", @@ -420,9 +405,9 @@ "SET MAX": "SET MAX", "Set recommended": "Set recommended", "Settings": "Settings", - "Settings > Preferences": "Settings > Preferences", "Show recovery phrase": "Show recovery phrase", "Sign": "Sign", + "Sign anyway": "Sign anyway", "Sign Transaction": "Sign Transaction", "Signed Payload": "Signed Payload", "Signer": "Signer", @@ -449,7 +434,6 @@ "Swap failed": "Swap failed", "swapped": "swapped", "Swapped": "Swapped", - "Swapped {{srcAssetCode}} for {{destAssetCode}}": "Swapped {{srcAssetCode}} for {{destAssetCode}}", "Switch to this network": "Switch to this network", "Terms of Service": "Terms of Service", "Terms of Use": "Terms of Use", @@ -470,7 +454,6 @@ } }, "them noted to confirm you got them right": "them noted to confirm you got them right", - "this alert by going to": "this alert by going to", "This asset has a balance": "This asset has a balance", "This asset has buying liabilities": "This asset has buying liabilities", "This asset is not part of an asset list": { @@ -481,9 +464,6 @@ } }, "This asset is part of the asset lists": "This asset is part of the asset lists", - "This asset was tagged as fraudulent by stellar": { - "expert, a reliable community-maintained directory": "This asset was tagged as fraudulent by stellar.expert, a reliable community-maintained directory." - }, "This authorization uses the source account's credentials, so you are implicitly authorizing this when you sign the transaction": "This authorization uses the source account's credentials, so you are implicitly authorizing this when you sign the transaction.", "This can be used to sign arbitrary transaction hashes without having to decode them first": { " Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust": "This can be used to sign arbitrary transaction hashes without having to decode them first. Ledger will not display the transaction details in the device display prior to signing so make sure you only interact with applications you know and trust." @@ -493,6 +473,9 @@ "This new backup phrase will be used for your new accounts": "This new backup phrase will be used for your new accounts.", "This site has been scanned and verified": "This site has been scanned and verified", "This site was flagged as malicious": "This site was flagged as malicious", + "This token was flagged as malicious by Blockaid": { + " Interacting with this token may result in loss of funds and is not recommended for the following reasons": "This token was flagged as malicious by Blockaid. Interacting with this token may result in loss of funds and is not recommended for the following reasons" + }, "This transaction could not be completed": "This transaction could not be completed.", "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.", @@ -507,6 +490,7 @@ "Transaction": "Transaction", "Transaction failed": "Transaction failed", "Transaction fee": "Transaction fee", + "Transaction Request": "Transaction Request", "Transaction timeout": "Transaction timeout", "Transactions": "Transactions", "trustlines": "trustlines", @@ -546,7 +530,6 @@ "You are interacting with data that may be using untested and changing schemas": { " Proceed at your own risk": "You are interacting with data that may be using untested and changing schemas. Proceed at your own risk." }, - "You can": "You can", "You can choose to merge your current account into the new accounts after the migration, which will effectively destroy your current account": { " Merging is optional and will allow you to send your current account’s funding lumens to the new accounts": "You can choose to merge your current account into the new accounts after the migration, which will effectively destroy your current account. Merging is optional and will allow you to send your current account’s funding lumens to the new accounts." }, diff --git a/extension/src/popup/views/GrantAccess/index.tsx b/extension/src/popup/views/GrantAccess/index.tsx index f3dac8ac4..025320453 100644 --- a/extension/src/popup/views/GrantAccess/index.tsx +++ b/extension/src/popup/views/GrantAccess/index.tsx @@ -8,7 +8,7 @@ import { getUrlHostname, parsedSearchParam } from "helpers/urls"; import { rejectAccess, grantAccess } from "popup/ducks/access"; import { publicKeySelector } from "popup/ducks/accountServices"; import { ButtonsContainer, ModalWrapper } from "popup/basics/Modal"; -import { ModalInfo } from "popup/components/ModalInfo"; +import { DomainScanModalInfo } from "popup/components/ModalInfo"; import { KeyIdenticon } from "popup/components/identicons/KeyIdenticon"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; import { useScanSite } from "popup/helpers/blockaid"; @@ -56,7 +56,7 @@ export const GrantAccess = () => {
) : ( - { )} - + )} diff --git a/extension/src/popup/views/IntegrationTest.tsx b/extension/src/popup/views/IntegrationTest.tsx index 6f539d660..0983ccbb0 100644 --- a/extension/src/popup/views/IntegrationTest.tsx +++ b/extension/src/popup/views/IntegrationTest.tsx @@ -35,7 +35,6 @@ import { addCustomNetwork, removeCustomNetwork, editCustomNetwork, - getBlockedDomains, } from "@shared/api/internal"; import { requestPublicKey, @@ -362,11 +361,6 @@ export const IntegrationTest = () => { assertEq(res.networksList.length, networksListLength - 1); }); - res = await getBlockedDomains(); - runAsserts("getBlockedDomains", () => { - assertEq(Object.keys(res.blockedDomains as object).length > 0, true); - }); - await changeNetwork(NETWORK_NAMES.PUBNET); res = await requestPublicKey(); runAsserts("requestPublicKey", () => { diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index 9319a11f9..212278d22 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -51,7 +51,7 @@ import { WarningMessageVariant, WarningMessage, FirstTimeWarningMessage, - FlaggedWarningMessage, + MemoWarningMessage, SSLWarningMessage, BlockaidMaliciousTxWarning, } from "popup/components/WarningMessages"; @@ -141,12 +141,6 @@ export const SignTransaction = () => { ); const flaggedKeyValues = Object.values(flaggedKeys); - const isUnsafe = flaggedKeyValues.some(({ tags }) => - tags.includes(TRANSACTION_WARNING.unsafe), - ); - const isMalicious = flaggedKeyValues.some(({ tags }) => - tags.includes(TRANSACTION_WARNING.malicious), - ); const isMemoRequired = flaggedKeyValues.some( ({ tags }) => tags.includes(TRANSACTION_WARNING.memoRequired) && !memo, ); @@ -190,13 +184,7 @@ export const SignTransaction = () => { if (isMemoRequired) { emitMetric(METRIC_NAMES.signTransactionMemoRequired); } - if (isUnsafe) { - emitMetric(METRIC_NAMES.signTransactionUnsafe); - } - if (isMalicious) { - emitMetric(METRIC_NAMES.signTransactionMalicious); - } - }, [isMemoRequired, isMalicious, isUnsafe]); + }, [isMemoRequired]); useEffect(() => { if (currentAccount.publicKey) { @@ -212,7 +200,7 @@ export const SignTransaction = () => { }; }, [currentAccount.publicKey, dispatch, networkDetails]); - const isSubmitDisabled = isMemoRequired || isMalicious; + const isSubmitDisabled = isMemoRequired; if (_networkPassphrase !== networkPassphrase) { return ( @@ -324,13 +312,7 @@ export const SignTransaction = () => {
) : null} - {flaggedKeyValues.length ? ( - - ) : null} + {!isDomainListedAllowed && !isSubmitDisabled ? ( ) : null} From e660533de5ee8bc8680e93ebb465191c25cf41c6 Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Fri, 30 Aug 2024 08:30:25 -0600 Subject: [PATCH 12/67] removes configurable fee constraint on token transfers --- .../SendSettings/Settings/index.tsx | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx index 5c6b87cf5..b2f3260ef 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx @@ -190,52 +190,50 @@ export const Settings = ({
- {!isToken ? ( -
-
- - {t("Maximum network transaction fee to be paid")}{" "} - - {t("Learn more")} - - - } - placement="bottom" - > - { - submitForm(); - handleTxFeeNav(); - }} - > - {t("Transaction fee")} +
+
+ + {t("Maximum network transaction fee to be paid")}{" "} + + {t("Learn more")} + - -
-
{ - submitForm(); - handleTxFeeNav(); - }} + } + placement="bottom" > - - {transactionFee} XLM + { + submitForm(); + handleTxFeeNav(); + }} + > + {t("Transaction fee")} -
- -
+ +
+
{ + submitForm(); + handleTxFeeNav(); + }} + > + + {transactionFee} XLM + +
+
- ) : null} +
From df27b1db55aa73b0c93a8fb2f8a4505f99529445 Mon Sep 17 00:00:00 2001 From: Aristides Staffieri Date: Tue, 3 Sep 2024 11:13:32 -0600 Subject: [PATCH 13/67] take into account min resource fee for toke payments --- .../SendSettings/Settings/index.tsx | 529 ++++++++++-------- .../popup/components/sendPayment/styles.scss | 8 + extension/src/popup/ducks/token-payment.ts | 11 +- 3 files changed, 318 insertions(+), 230 deletions(-) diff --git a/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx index b2f3260ef..1954f389e 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx @@ -1,15 +1,20 @@ -import React, { useEffect } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { Formik, Form, Field, FieldProps } from "formik"; -import { Icon, Textarea, Link, Button } from "@stellar/design-system"; +import { Icon, Textarea, Link, Button, Loader } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; -import { Asset } from "stellar-sdk"; +import { Asset, BASE_FEE } from "stellar-sdk"; +import BigNumber from "bignumber.js"; import { navigateTo } from "popup/helpers/navigate"; import { useNetworkFees } from "popup/helpers/useNetworkFees"; import { useIsSwap } from "popup/helpers/useIsSwap"; import { getNativeContractDetails } from "popup/helpers/searchAsset"; -import { isMuxedAccount, getAssetFromCanonical } from "helpers/stellar"; +import { + isMuxedAccount, + getAssetFromCanonical, + stroopToXlm, +} from "helpers/stellar"; import { ROUTES } from "popup/constants/routes"; import { SubviewHeader } from "popup/components/SubviewHeader"; import { FormRows } from "popup/basics/Forms"; @@ -23,12 +28,21 @@ import { transactionSubmissionSelector, saveIsToken, } from "popup/ducks/transactionSubmission"; -import { simulateTokenPayment, simulateSwap } from "popup/ducks/token-payment"; +import { + simulateTokenPayment, + simulateSwap, + tokenSimulationSelector, +} from "popup/ducks/token-payment"; import { InfoTooltip } from "popup/basics/InfoTooltip"; import { publicKeySelector } from "popup/ducks/accountServices"; import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; -import { parseTokenAmount, isContractId } from "popup/helpers/soroban"; +import { + parseTokenAmount, + isContractId, + formatTokenAmount, + CLASSIC_ASSET_DECIMALS, +} from "popup/helpers/soroban"; import { Balances, TokenBalance } from "@shared/api/types"; import { AppDispatch } from "popup/App"; @@ -62,36 +76,15 @@ export const Settings = ({ const isPathPayment = useSelector(isPathPaymentSelector); const publicKey = useSelector(publicKeySelector); const { accountBalances } = useSelector(transactionSubmissionSelector); + const { simulation } = useSelector(tokenSimulationSelector); const isSwap = useIsSwap(); const { recommendedFee } = useNetworkFees(); + const [isLoadingSimulation, setLoadingSimulation] = useState(true); - // use default transaction fee if unset - useEffect(() => { - if (!transactionFee) { - dispatch(saveTransactionFee(recommendedFee)); - } - }, [dispatch, recommendedFee, transactionFee]); - - const handleTxFeeNav = () => - navigateTo(isSwap ? ROUTES.swapSettingsFee : ROUTES.sendPaymentSettingsFee); - - const handleSlippageNav = () => - navigateTo( - isSwap ? ROUTES.swapSettingsSlippage : ROUTES.sendPaymentSettingsSlippage, - ); - - const handleTimeoutNav = () => - navigateTo( - isSwap ? ROUTES.swapSettingsTimeout : ROUTES.sendPaymentSettingsTimeout, - ); - - // dont show memo for regular sends to Muxed, or for swaps - const showMemo = !isSwap && !isMuxedAccount(destination); - const showSlippage = (isPathPayment || isSwap) && !isSoroswap; const isSendSacToContract = isContractId(destination) && !isContractId(getAssetFromCanonical(asset).issuer); - const getSacContractAddress = () => { + const getSacContractAddress = useCallback(() => { if (asset === "native") { return getNativeContractDetails(networkDetails).contract; } @@ -105,74 +98,152 @@ export const Settings = ({ ); return contractAddress; - }; + }, [asset, networkDetails]); - async function goToReview() { - if (isSoroswap) { - const simulatedTx = await dispatch( - simulateSwap({ - networkDetails, - publicKey, - amountIn: amount, - amountInDecimals: decimals || 0, - amountOut: destinationAmount, - amountOutDecimals: destinationDecimals || 0, - memo, - transactionFee, - path, - }), + useEffect(() => { + async function simulateTx() { + // use default transaction fee if unset + const baseFee = new BigNumber( + transactionFee || recommendedFee || stroopToXlm(BASE_FEE), ); - if (simulateSwap.fulfilled.match(simulatedTx)) { - dispatch(saveSimulation(simulatedTx.payload)); - navigateTo(next); - } - return; - } + if (isSoroswap) { + const simulatedTx = await dispatch( + simulateSwap({ + networkDetails, + publicKey, + amountIn: amount, + amountInDecimals: decimals || 0, + amountOut: destinationAmount, + amountOutDecimals: destinationDecimals || 0, + memo, + transactionFee, + path, + }), + ); - if (isToken || isSendSacToContract) { - const assetAddress = isSendSacToContract - ? getSacContractAddress() - : asset.split(":")[1]; - const balances = - accountBalances.balances || ({} as NonNullable); - const assetBalance = balances[asset] as TokenBalance; - - if (!assetBalance) { - throw new Error("Asset Balance not available"); + if (simulateSwap.fulfilled.match(simulatedTx)) { + dispatch(saveSimulation(simulatedTx.payload)); + const minResourceFee = formatTokenAmount( + new BigNumber( + simulatedTx.payload.simulationTransaction.minResourceFee, + ), + CLASSIC_ASSET_DECIMALS, + ); + dispatch( + saveTransactionFee( + baseFee.plus(new BigNumber(minResourceFee)).toString(), + ), + ); + } + return; } - const parsedAmount = isSendSacToContract - ? parseTokenAmount(amount, 7) - : parseTokenAmount(amount, Number(assetBalance.decimals)); + if ( + (isToken || isSendSacToContract) && + !simulation.simulationTransaction?.minResourceFee + ) { + const assetAddress = isSendSacToContract + ? getSacContractAddress() + : asset.split(":")[1]; + const balances = + accountBalances.balances || ({} as NonNullable); + const assetBalance = balances[asset] as TokenBalance; + + if (!assetBalance) { + throw new Error("Asset Balance not available"); + } - const params = { - publicKey, - destination, - amount: parsedAmount.toNumber(), - }; + const parsedAmount = isSendSacToContract + ? parseTokenAmount(amount, CLASSIC_ASSET_DECIMALS) + : parseTokenAmount(amount, Number(assetBalance.decimals)); - const simulation = await dispatch( - simulateTokenPayment({ - address: assetAddress, + const params = { publicKey, - memo, - params, - networkDetails, - transactionFee, - }), - ); + destination, + amount: parsedAmount.toNumber(), + }; + + const simResponse = await dispatch( + simulateTokenPayment({ + address: assetAddress, + publicKey, + memo, + params, + networkDetails, + transactionFee, + }), + ); + + if ( + simulateTokenPayment.fulfilled.match(simResponse) && + recommendedFee + ) { + const minResourceFee = formatTokenAmount( + new BigNumber( + simResponse.payload.simulationTransaction.minResourceFee, + ), + CLASSIC_ASSET_DECIMALS, + ); + dispatch(saveSimulation(simResponse.payload)); + dispatch(saveIsToken(true)); + dispatch( + saveTransactionFee( + baseFee.plus(new BigNumber(minResourceFee)).toString(), + ), + ); + } + return; + } - if (simulateTokenPayment.fulfilled.match(simulation)) { - dispatch(saveSimulation(simulation.payload)); - dispatch(saveIsToken(true)); - navigateTo(next); + if (!transactionFee) { + dispatch(saveTransactionFee(baseFee.toString())); } - return; } + async function setFee() { + setLoadingSimulation(true); + await simulateTx(); + setLoadingSimulation(false); + } + setFee(); + }, [ + dispatch, + recommendedFee, + transactionFee, + accountBalances.balances, + amount, + asset, + decimals, + destination, + destinationAmount, + destinationDecimals, + getSacContractAddress, + isSendSacToContract, + isSoroswap, + isToken, + memo, + networkDetails, + path, + publicKey, + simulation.simulationTransaction?.minResourceFee, + ]); - navigateTo(next); - } + const handleTxFeeNav = () => + navigateTo(isSwap ? ROUTES.swapSettingsFee : ROUTES.sendPaymentSettingsFee); + + const handleSlippageNav = () => + navigateTo( + isSwap ? ROUTES.swapSettingsSlippage : ROUTES.sendPaymentSettingsSlippage, + ); + + const handleTimeoutNav = () => + navigateTo( + isSwap ? ROUTES.swapSettingsTimeout : ROUTES.sendPaymentSettingsTimeout, + ); + + // dont show memo for regular sends to Muxed, or for swaps + const showMemo = !isSwap && !isMuxedAccount(destination); + const showSlippage = (isPathPayment || isSwap) && !isSoroswap; return ( @@ -180,117 +251,74 @@ export const Settings = ({ title={`${isSwap ? t("Swap") : t("Send")} ${t("Settings")}`} customBackAction={() => navigateTo(previous)} /> - { - dispatch(saveMemo(values.memo)); - }} - > - {({ submitForm }) => ( - - - -
-
- - {t("Maximum network transaction fee to be paid")}{" "} - - {t("Learn more")} - - - } - placement="bottom" - > - { - submitForm(); - handleTxFeeNav(); - }} + {isLoadingSimulation ? ( +
+ +
+ ) : ( + { + dispatch(saveMemo(values.memo)); + }} + > + {({ submitForm }) => ( + + + +
+
+ + {t("Maximum network transaction fee to be paid")}{" "} + + {t("Learn more")} + + + } + placement="bottom" > - {t("Transaction fee")} - - -
-
{ - submitForm(); - handleTxFeeNav(); - }} - > - - {transactionFee} XLM - -
- -
-
-
- -
-
- - {t( - "Number of seconds that can pass before this transaction can no longer be accepted by the network", - )}{" "} + { + submitForm(); + handleTxFeeNav(); + }} + > + {t("Transaction fee")} - } - placement="bottom" + +
+
{ + submitForm(); + handleTxFeeNav(); + }} > - { - submitForm(); - handleTimeoutNav(); - }} - > - {t("Transaction timeout")} + + {transactionFee} XLM - -
-
{ - submitForm(); - handleTimeoutNav(); - }} - > - - {transactionTimeout}(s) - -
- +
+ +
-
- {showSlippage && (
{t( - "Allowed downward variation in the destination amount", + "Number of seconds that can pass before this transaction can no longer be accepted by the network", )}{" "} - - {t("Learn more")} - } placement="bottom" @@ -299,10 +327,10 @@ export const Settings = ({ className="SendSettings__row__title SendSettings__clickable" onClick={() => { submitForm(); - handleSlippageNav(); + handleTimeoutNav(); }} > - {t("Allowed slippage")} + {t("Transaction timeout")}
@@ -310,29 +338,30 @@ export const Settings = ({ className="SendSettings__row__right SendSettings__clickable" onClick={() => { submitForm(); - handleSlippageNav(); + handleTimeoutNav(); }} > - - {allowedSlippage}% + + {transactionTimeout}(s)
- )} - {showMemo && ( - <> + + {showSlippage && (
- {t("Include a custom memo to this transaction")}{" "} + {t( + "Allowed downward variation in the destination amount", + )}{" "} @@ -342,45 +371,93 @@ export const Settings = ({ } placement="bottom" > - - {t("Memo")} + { + submitForm(); + handleSlippageNav(); + }} + > + {t("Allowed slippage")}
-
- +
{ + submitForm(); + handleSlippageNav(); + }} + > + + {allowedSlippage}% + +
+ +
- - {({ field }: FieldProps) => ( -