diff --git a/packages/mask/content-script/site-adaptors/twitter.com/utils/fetch.ts b/packages/mask/content-script/site-adaptors/twitter.com/utils/fetch.ts index 84ff7653bedc..7a273cdb8443 100644 --- a/packages/mask/content-script/site-adaptors/twitter.com/utils/fetch.ts +++ b/packages/mask/content-script/site-adaptors/twitter.com/utils/fetch.ts @@ -97,12 +97,28 @@ function resolveType(content: string) { return 'normal' } +function getVisibleText(node: Node) { + if (node.nodeType === Node.TEXT_NODE) return node.textContent + if (node.nodeType !== Node.ELEMENT_NODE) return '' + if ( + (node as Element).getAttribute('aria-hidden') === 'true' && // getBoundingClientRect() causes layout, getAttribute() is cheaper + (node as Element).getBoundingClientRect().width === 0 + ) + return '' + let text = '' + for (const child of node.childNodes) { + text += getVisibleText(child) + } + return text +} + export function postContentMessageParser(node: HTMLElement): TypedMessage { function make(node: Node): TypedMessage { if (node.nodeType === Node.TEXT_NODE) { if (!node.nodeValue) return makeTypedMessageEmpty() return makeTypedMessageText(node.nodeValue, getElementStyle(node.parentElement)) - } else if (node instanceof HTMLAnchorElement) { + } + if (node instanceof HTMLAnchorElement) { const anchor = node const href = anchor.getAttribute('title') ?? anchor.getAttribute('href') if (href?.includes('/photo/')) { @@ -131,7 +147,7 @@ export function postContentMessageParser(node: HTMLElement): TypedMessage { makeTypedMessageEmpty(), ) } - const content = anchor.textContent + const content = getVisibleText(anchor) if (!content) return makeTypedMessageEmpty() const altImage = node.querySelector('img') return makeTypedMessageAnchor( diff --git a/packages/plugins/ScamWarning/src/SiteAdaptor/components/LinkModifier.tsx b/packages/plugins/ScamWarning/src/SiteAdaptor/components/LinkModifier.tsx index 7d1ec568b929..374c72771fe1 100644 --- a/packages/plugins/ScamWarning/src/SiteAdaptor/components/LinkModifier.tsx +++ b/packages/plugins/ScamWarning/src/SiteAdaptor/components/LinkModifier.tsx @@ -10,14 +10,23 @@ import { usePopoverControl } from './usePopoverControl.js' import { WarningCard } from './WarningCard.js' import { SecurityProvider } from '../../constants.js' import { GoPlusLabs } from '@masknet/web3-providers' +import { extractAddresses } from '../../utils.js' +import { useDetectAddress } from '../hooks/useDetectAddress.js' +import { AddressTag } from './TextModifier.js' -const useStyles = makeStyles()({ +const useStyles = makeStyles()((theme) => ({ link: { whiteSpace: 'nowrap', display: 'inline-flex', alignItems: 'center', gap: 4, verticalAlign: 'bottom', + '& > a': { + color: theme.palette.maskColor.danger, + }, + }, + address: { + display: 'contents', }, icon: { width: 18, @@ -26,20 +35,20 @@ const useStyles = makeStyles()({ overflow: 'hidden', cursor: 'pointer', }, -}) +})) function isTCO(url: string | null) { if (!url) return false return url.startsWith('https://t.co/') } -export const LinkModifier = memo>(function ModifyLink({ +export const LinkModifier = memo>(function LinkModifier({ fallback, ...props }) { const { classes } = useStyles() const { data } = useQuery({ - queryKey: ['scam-warning', 'check-link', props.href], + queryKey: ['scam-warning', 'check-link', props.href, props.children], queryFn: async () => { const resolvedLink = isTCO(props.href) ? await resolveTCOLink(props.href) : props.href if (!resolvedLink) return { isScam: false } @@ -50,16 +59,35 @@ export const LinkModifier = memo + + + {props.children} + {props.suggestedPostImage} + + + ) + } + return fallback + } return ( diff --git a/packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx b/packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx index 7bb29ef994ba..1e2985ab8ede 100644 --- a/packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx +++ b/packages/plugins/ScamWarning/src/SiteAdaptor/components/TextModifier.tsx @@ -1,24 +1,20 @@ import { Icons } from '@masknet/icons' import type { Plugin } from '@masknet/plugin-infra' import { makeStyles, ShadowRootPopper } from '@masknet/theme' -import { GoPlusLabs } from '@masknet/web3-providers' -import { isValidAddress } from '@masknet/web3-shared-evm' -import { isValidAddress as isSolAddress } from '@masknet/web3-shared-solana' -import { useQuery } from '@tanstack/react-query' import { Fragment, memo, useMemo } from 'react' -import { EVM_ADDRESS, SecurityProvider, SOLANA_ADDRESS, TRON_ADDRESS } from '../../constants.js' -import { PluginScamRPC } from '../../messages.js' -import { isTronAddress } from '../../utils.js' +import { extractAddresses } from '../../utils.js' import { usePopoverControl } from './usePopoverControl.js' import { WarningCard } from './WarningCard.js' +import { useDetectAddress } from '../hooks/useDetectAddress.js' -const useStyles = makeStyles()({ +const useStyles = makeStyles()((theme) => ({ text: { whiteSpace: 'nowrap', display: 'inline-flex', alignItems: 'center', gap: 4, verticalAlign: 'bottom', + color: theme.palette.maskColor.danger, }, icon: { width: 18, @@ -27,40 +23,25 @@ const useStyles = makeStyles()({ overflow: 'hidden', cursor: 'pointer', }, -}) +})) type TextModifierProps = PropsOf interface AddressTagProps { address: string text: string + nested?: boolean + className?: string } -const AddressTag = memo(function AddressTag({ address, text }) { - const { classes } = useStyles() +export const AddressTag = memo(function AddressTag({ address, text, className, nested }) { + const { classes, cx } = useStyles() const { open, anchorEl, iconRef, onMouseEnter, onMouseLeave } = usePopoverControl() - const { data } = useQuery({ - queryKey: ['detect-address', address], - queryFn: async () => { - if (isValidAddress(address)) { - return { isScam: await PluginScamRPC.checkAddress(address), provider: SecurityProvider.ScamSniffer } - } - if (isSolAddress(address)) - return { - isScam: await GoPlusLabs.checkIfAddressIsScam('solana', address), - provider: SecurityProvider.GoPlus, - } - if (isTronAddress(address)) - return { - isScam: GoPlusLabs.checkIfAddressIsScam('tron', address), - provider: SecurityProvider.GoPlus, - } - return { isScam: false, provider: null } - }, - }) + const { data } = useDetectAddress(address) + if (!data?.isScam) return text return ( - + (function AddressTag({ address, text }) onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} /> - {address} + {nested ? null : address} (function TextModifier({ fallback, children: fullText }) { - const addresses = useMemo(() => { - const evmAddresses = fullText.match(EVM_ADDRESS) || [] - const solAddresses = fullText.match(SOLANA_ADDRESS) || [] - const tronAddresses = fullText.match(TRON_ADDRESS) || [] - return [...evmAddresses, ...solAddresses, ...tronAddresses] - }, [fullText]) + const addresses = useMemo(() => extractAddresses(fullText), [fullText]) const segments = useMemo(() => { let leftOffset = 0 diff --git a/packages/plugins/ScamWarning/src/SiteAdaptor/components/WarningCard.tsx b/packages/plugins/ScamWarning/src/SiteAdaptor/components/WarningCard.tsx index ee9cc29946f2..cc78123b851f 100644 --- a/packages/plugins/ScamWarning/src/SiteAdaptor/components/WarningCard.tsx +++ b/packages/plugins/ScamWarning/src/SiteAdaptor/components/WarningCard.tsx @@ -1,9 +1,9 @@ import { t } from '@lingui/core/macro' import { Trans } from '@lingui/react/macro' import { Icons } from '@masknet/icons' -import { LoadingBase, makeStyles } from '@masknet/theme' -import { Button, Typography } from '@mui/material' -import { memo, useState, type HTMLProps } from 'react' +import { makeStyles } from '@masknet/theme' +import { Typography } from '@mui/material' +import { memo, type HTMLProps } from 'react' import { SecurityProvider } from '../../constants.js' const useStyles = makeStyles()((theme) => ({ @@ -65,22 +65,11 @@ const useStyles = makeStyles()((theme) => ({ minWidth: 0, textOverflow: 'ellipsis', overflow: 'hidden', + whiteSpace: 'nowrap', }, link: { textDecoration: 'underline', }, - reportButton: { - padding: theme.spacing(1, 0), - width: 60, - minWidth: 60, - textAlign: 'center', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - color: theme.palette.maskColor.main, - backgroundColor: theme.palette.maskColor.thirdMain, - marginLeft: 'auto', - }, description: { fontFamily: 'Helvetica', borderRadius: 8, @@ -101,7 +90,6 @@ interface Props extends HTMLProps { export const WarningCard = memo(function WarningCard({ link, address, securityProvider, ...rest }: Props) { const { classes, cx } = useStyles() - const [isReporting] = useState(false) return (
@@ -130,15 +118,13 @@ export const WarningCard = memo(function WarningCard({ link, address, securityPr
{link ? - + {link} - : {address}} - + : + {address} + + }
diff --git a/packages/plugins/ScamWarning/src/SiteAdaptor/hooks/useCheckLink.ts b/packages/plugins/ScamWarning/src/SiteAdaptor/hooks/useCheckLink.ts new file mode 100644 index 000000000000..b26ce099eea0 --- /dev/null +++ b/packages/plugins/ScamWarning/src/SiteAdaptor/hooks/useCheckLink.ts @@ -0,0 +1,38 @@ +import { resolveTCOLink } from '@masknet/plugin-infra/dom/context' +import { GoPlusLabs } from '@masknet/web3-providers' +import { useQuery } from '@tanstack/react-query' +import { SecurityProvider } from '../../constants.js' +import { PluginScamRPC } from '../../messages.js' +import { extractAddresses } from '../../utils.js' + +function isTCO(url: string | null) { + if (!url) return false + return url.startsWith('https://t.co/') +} + +export function useCheckLink(link: string, text: string) { + return useQuery({ + queryKey: ['scam-warning', 'check-link', link, text], + queryFn: async () => { + const resolvedLink = isTCO(link) ? await resolveTCOLink(link) : link + if (!resolvedLink) return { isScam: false } + const result = await GoPlusLabs.checkIsPhishingSite(resolvedLink) + if (result) + return { + isScam: result, + provider: SecurityProvider.GoPlus, + resolvedLink, + } + const isEllipsis = text.endsWith('…') + // We assume that the link contains only one address + const address = isEllipsis ? extractAddresses(resolvedLink)[0] : undefined + + return { + isScam: await PluginScamRPC.checkUrl(resolvedLink), + provider: SecurityProvider.ScamSniffer, + resolvedLink, + address, + } + }, + }) +} diff --git a/packages/plugins/ScamWarning/src/SiteAdaptor/hooks/useDetectAddress.ts b/packages/plugins/ScamWarning/src/SiteAdaptor/hooks/useDetectAddress.ts new file mode 100644 index 000000000000..48cab02f0438 --- /dev/null +++ b/packages/plugins/ScamWarning/src/SiteAdaptor/hooks/useDetectAddress.ts @@ -0,0 +1,31 @@ +import { GoPlusLabs } from '@masknet/web3-providers' +import { isValidAddress } from '@masknet/web3-shared-evm' +import { isValidAddress as isSolAddress } from '@masknet/web3-shared-solana' +import { useQuery } from '@tanstack/react-query' +import { SecurityProvider } from '../../constants.js' +import { PluginScamRPC } from '../../messages.js' +import { isTronAddress } from '../../utils.js' + +export function useDetectAddress(address: string | null | undefined, enabled = true) { + return useQuery({ + enabled: !!address && enabled, + queryKey: ['detect-address', address], + queryFn: async () => { + if (!address) return null + if (isValidAddress(address)) { + return { isScam: await PluginScamRPC.checkAddress(address), provider: SecurityProvider.ScamSniffer } + } + if (isSolAddress(address)) + return { + isScam: await GoPlusLabs.checkIfAddressIsScam('solana', address), + provider: SecurityProvider.GoPlus, + } + if (isTronAddress(address)) + return { + isScam: GoPlusLabs.checkIfAddressIsScam('tron', address), + provider: SecurityProvider.GoPlus, + } + return { isScam: false, provider: null } + }, + }) +} diff --git a/packages/plugins/ScamWarning/src/constants.ts b/packages/plugins/ScamWarning/src/constants.ts index 2b9b92469814..18f94f0122e8 100644 --- a/packages/plugins/ScamWarning/src/constants.ts +++ b/packages/plugins/ScamWarning/src/constants.ts @@ -8,6 +8,10 @@ export const EVM_ADDRESS = /(^|\s)(0x[a-fA-F0-9]{40})/gu export const SOLANA_ADDRESS = /(^|\s)([1-9A-HJ-NP-Za-km-z]{32,44})/gu export const TRON_ADDRESS = /(^|\s)(T[A-Za-z1-9]{33})/gu +export const EXIST_EVM_ADDRESS = /\b(0x[a-fA-F0-9]{40})/gu +export const EXIST_SOLANA_ADDRESS = /\b([1-9A-HJ-NP-Za-km-z]{32,44})/gu +export const EXIST_TRON_ADDRESS = /\b(T[A-Za-z1-9]{33})/gu + export enum SecurityProvider { ScamSniffer = 'ScamSniffer', GoPlus = 'GoPlus', diff --git a/packages/plugins/ScamWarning/src/utils.ts b/packages/plugins/ScamWarning/src/utils.ts index edf05a7980d0..e2a769f33d12 100644 --- a/packages/plugins/ScamWarning/src/utils.ts +++ b/packages/plugins/ScamWarning/src/utils.ts @@ -1,3 +1,13 @@ +import { EVM_ADDRESS, SOLANA_ADDRESS } from '@masknet/plugin-scam-warning' +import { EXIST_EVM_ADDRESS, EXIST_SOLANA_ADDRESS, EXIST_TRON_ADDRESS, TRON_ADDRESS } from './constants.js' + export function isTronAddress(address: string) { return !!address.match(address) } + +export function extractAddresses(text: string, exist = false) { + const evmAddresses = text.match(exist ? EXIST_EVM_ADDRESS : EVM_ADDRESS) || [] + const solAddresses = text.match(exist ? EXIST_SOLANA_ADDRESS : SOLANA_ADDRESS) || [] + const tronAddresses = text.match(exist ? EXIST_TRON_ADDRESS : TRON_ADDRESS) || [] + return [...evmAddresses, ...solAddresses, ...tronAddresses] +} diff --git a/packages/web3-constants/evm/nft-red-packet.json b/packages/web3-constants/evm/nft-red-packet.json index 86f2f645549e..d068e3f5f464 100644 --- a/packages/web3-constants/evm/nft-red-packet.json +++ b/packages/web3-constants/evm/nft-red-packet.json @@ -39,10 +39,10 @@ "Base_Goerli": 0, "Polygon": 19131916, "Mumbai": 0, - "Arbitrum": 0, + "Arbitrum": 909051, "Arbitrum_Rinkeby": 0, - "xDai": 0, - "Optimism": 0, + "xDai": 18101937, + "Optimism": 2282849, "Optimism_Kovan": 0, "Optimism_Goerli": 0, "Avalanche": 0,