From 5f3aa455812d434bcc769e5c7d9e5a741737aa2e Mon Sep 17 00:00:00 2001 From: Skylar Barrera Date: Mon, 30 Oct 2023 12:49:53 -0400 Subject: [PATCH] WC: dapp warnings (#5147) * init dapp warnings * fix info alert title weight * query clean up * info alert * i18n * rm logs * types + authRequests * i18n + verified context for wc connections * fix text padding * clean up * lint --- package.json | 1 + shim.js | 1 + src/components/info-alert/info-alert.tsx | 6 +- src/graphql/queries/metadata.graphql | 16 ++ src/languages/en_US.json | 6 + src/redux/walletconnect.ts | 2 + src/resources/metadata/dapp.ts | 109 +++++++++++ src/screens/WalletConnectApprovalSheet.js | 38 +++- src/utils/connectedApps.ts | 215 ++++++++++++++++++++++ src/walletConnect/index.tsx | 15 +- src/walletConnect/sheets/AuthRequest.tsx | 120 ++++++++---- yarn.lock | 21 +++ 12 files changed, 508 insertions(+), 42 deletions(-) create mode 100644 src/resources/metadata/dapp.ts create mode 100644 src/utils/connectedApps.ts diff --git a/package.json b/package.json index 0a4e28b7f49..8828f7a0383 100644 --- a/package.json +++ b/package.json @@ -270,6 +270,7 @@ "react-native-tooltip": "rainbow-me/react-native-tooltip#e0e88d212b5b7f350e5eabba87f588a32e0f2590", "react-native-tooltips": "rainbow-me/react-native-tooltips#d278f506782d3f39d68ae830c92b8afe670f0e3b", "react-native-udp": "2.7.0", + "react-native-url-polyfill": "2.0.0", "react-native-version-number": "0.3.6", "react-native-video": "5.2.1", "react-native-video-cache": "2.0.5", diff --git a/shim.js b/shim.js index 14dcce93e3b..5201d4cff03 100644 --- a/shim.js +++ b/shim.js @@ -1,4 +1,5 @@ import 'react-native-get-random-values'; +import 'react-native-url-polyfill/auto'; import '@ethersproject/shims'; import AsyncStorage from '@react-native-async-storage/async-storage'; import ReactNative from 'react-native'; diff --git a/src/components/info-alert/info-alert.tsx b/src/components/info-alert/info-alert.tsx index 4dd3a9ec84e..9b3300341ff 100644 --- a/src/components/info-alert/info-alert.tsx +++ b/src/components/info-alert/info-alert.tsx @@ -17,7 +17,7 @@ export const InfoAlert: React.FC = ({ style={{ gap: 12, borderWidth: 2, - borderColor: useForegroundColor('separatorTertiary'), + borderColor: 'red', }} flexDirection="row" borderRadius={20} @@ -29,8 +29,8 @@ export const InfoAlert: React.FC = ({ {rightIcon} - - + + {title} diff --git a/src/graphql/queries/metadata.graphql b/src/graphql/queries/metadata.graphql index ed4f8d55755..3bfdc661b0c 100644 --- a/src/graphql/queries/metadata.graphql +++ b/src/graphql/queries/metadata.graphql @@ -102,3 +102,19 @@ query reverseResolveENSProfile( } } } + +query getdApp($shortName: String!, $url: String!, $status: Boolean!) { + dApp(shortName: $shortName, url: $url) { + name + status @include(if: $status) + colors { + primary + fallback + shadow + } + iconURL + url + description + shortName + } +} diff --git a/src/languages/en_US.json b/src/languages/en_US.json index aded3cbad93..1f1341b3c23 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2298,6 +2298,12 @@ "request_invalid": "The request contained invalid parameters. Please try again or contact Rainbow and/or dapp support teams.", "request_unsupported_network": "The network specified in this request is not supported by Rainbow.", "request_unsupported_methods": "The RPC method(s) specified in this request are not supported by Rainbow." + }, + "dapp_warnings": { + "info_alert": { + "title": "This app is likely malicious", + "description": "Signing messages or transactions from this app could result in losing your assets" + } } }, "warning": { diff --git a/src/redux/walletconnect.ts b/src/redux/walletconnect.ts index cf833bff8d9..691a55cb1f6 100644 --- a/src/redux/walletconnect.ts +++ b/src/redux/walletconnect.ts @@ -30,6 +30,7 @@ import { getFCMToken } from '@/notifications/tokens'; import { logger, RainbowError } from '@/logger'; import { IS_DEV, IS_IOS, IS_TEST } from '@/env'; import { RainbowNetworks } from '@/networks'; +import { Verify } from '@walletconnect/types'; // -- Variables --------------------------------------- // let showRedirectSheetThreshold = 300; @@ -161,6 +162,7 @@ export interface WalletconnectApprovalSheetRouteParams { timeout?: ReturnType | null; timedOut?: boolean; failureExplainSheetVariant?: string; + verifiedData?: Verify.Context['verified']; } /** diff --git a/src/resources/metadata/dapp.ts b/src/resources/metadata/dapp.ts new file mode 100644 index 00000000000..0bb09169996 --- /dev/null +++ b/src/resources/metadata/dapp.ts @@ -0,0 +1,109 @@ +import { useQuery } from '@tanstack/react-query'; +import { metadataClient } from '@/graphql'; +import { DAppStatus } from '@/graphql/__generated__/metadata'; +import { QueryFunctionArgs, createQueryKey, queryClient } from '@/react-query'; + +import { + getDappHost, + getDappHostname, + getHardcodedDappInformation, + isValidUrl, +} from '@/utils/connectedApps'; +import { capitalize } from 'lodash'; + +export interface DappMetadata { + url: string; + appHost: string; + appHostName: string; + appName: string; + appShortName: string; + appLogo?: string; + timestamp?: number; + status?: DAppStatus; +} + +// /////////////////////////////////////////////// +// Query Types + +type DappMetadataArgs = { + url?: string; +}; + +// /////////////////////////////////////////////// +// Query Key + +const DappMetadataQueryKey = ({ url }: DappMetadataArgs) => + createQueryKey('dappMetadata', { url }, { persisterVersion: 1 }); + +type DappMetadataQueryKey = ReturnType; + +// /////////////////////////////////////////////// +// Query Function + +export async function fetchDappMetadata({ + url, + status, +}: { + url: string; + status: boolean; +}) { + const appHostName = url && isValidUrl(url) ? getDappHostname(url) : ''; + const hardcodedAppName = + url && isValidUrl(url) + ? getHardcodedDappInformation(appHostName)?.name || '' + : ''; + + const response = await metadataClient.getdApp({ + shortName: hardcodedAppName, + url, + status, + }); + + const appHost = url && isValidUrl(url) ? getDappHost(url) : ''; + const appName = response?.dApp?.name + ? capitalize(response?.dApp?.name) + : hardcodedAppName || appHost; + const appShortName = response?.dApp?.shortName + ? capitalize(response?.dApp?.shortName) + : appName; + const dappMetadata = { + url, + appHost, + appHostName, + appName, + appShortName, + appLogo: response?.dApp?.iconURL, + status: response.dApp?.status, + }; + return dappMetadata; +} + +export async function dappMetadataQueryFunction({ + queryKey: [{ url }], +}: QueryFunctionArgs< + typeof DappMetadataQueryKey +>): Promise { + if (!url) return null; + const dappMetadata = await fetchDappMetadata({ url, status: true }); + return dappMetadata; +} + +export async function prefetchDappMetadata({ url }: { url: string }) { + queryClient.prefetchQuery( + DappMetadataQueryKey({ url }), + async () => fetchDappMetadata({ url, status: false }), + { + staleTime: 60000, + } + ); +} + +// /////////////////////////////////////////////// +// Query Hook + +export function useDappMetadata({ url }: DappMetadataArgs) { + return useQuery(DappMetadataQueryKey({ url }), dappMetadataQueryFunction, { + cacheTime: 1000 * 60 * 60 * 24, + enabled: !!url, + }); +} diff --git a/src/screens/WalletConnectApprovalSheet.js b/src/screens/WalletConnectApprovalSheet.js index e1d4d25d74e..7a2cb8e6538 100644 --- a/src/screens/WalletConnectApprovalSheet.js +++ b/src/screens/WalletConnectApprovalSheet.js @@ -49,6 +49,9 @@ import { ETH_ADDRESS, ETH_SYMBOL } from '@/references'; import { AssetType } from '@/entities'; import { RainbowNetworks, getNetworkObj } from '@/networks'; import { IS_IOS } from '@/env'; +import { useDappMetadata } from '@/resources/metadata/dapp'; +import { DAppStatus } from '@/graphql/__generated__/metadata'; +import { InfoAlert } from '@/components/info-alert/info-alert'; const LoadingSpinner = styled(android ? Spinner : ActivityIndicator).attrs( ({ theme: { colors } }) => ({ @@ -253,6 +256,18 @@ export default function WalletConnectApprovalSheet() { const { dappName, dappUrl, dappScheme, imageUrl, peerId } = meta; + const verifiedData = params?.verifiedData; + const { data: metadata } = useDappMetadata({ + url: verifiedData?.verifyUrl || dappUrl, + }); + + const isScam = metadata?.status === DAppStatus.Scam; + + // disabling Verified for now + const isVerified = false; //metadata?.status === DAppStatus.Verified; + + const accentColor = isScam ? colors.red : colors.appleBlue; + useEffect(() => { return () => { clearTimeout(timeout); @@ -464,15 +479,34 @@ export default function WalletConnectApprovalSheet() { + {isScam && '􁅏 '} + {isVerified && '􀇻 '} {formattedDappUrl} + {isScam && ( + + + 􀘰 + + } + title={lang.t( + lang.l.walletconnect.dapp_warnings.info_alert.title + )} + description={lang.t( + lang.l.walletconnect.dapp_warnings.info_alert.description + )} + /> + + )} { + try { + new URL(url); + return true; + } catch (err) { + return false; + } +}; + +export const getDappHost = (url: string) => { + const host = new URL(url).host; + if (host.indexOf('www.') === 0) { + return host.replace('www.', ''); + } + return host; +}; + +export const getDappHostname = (url: string) => { + const urlObject = new URL(url); + let hostname; + const subdomains = urlObject.hostname.split('.'); + if (subdomains.length === 2) { + hostname = urlObject.hostname; + } else { + hostname = `${subdomains[subdomains.length - 2]}.${ + subdomains[subdomains.length - 1] + }`; + } + return hostname; +}; + +export const getPublicAppIcon = (host: string) => + `https://icons.duckduckgo.com/ip3/${host}.ico`; + +const displayDappNames: { + [name: string]: { name: string }; +} = { + '1inch.io': { + name: '1inch', + }, + '88mph.app': { + name: '88mph', + }, + 'aave.com': { + name: 'Aave', + }, + 'artblocks.io': { + name: 'Art Blocks', + }, + 'astrofrens.com': { + name: 'Astro Frens', + }, + 'badger.finance': { + name: 'Badger DAO', + }, + 'balancer.exchange': { + name: 'Balancer', + }, + 'blit.house': { + name: 'Blit House', + }, + 'blitmap.com': { + name: 'Blitmap', + }, + 'collab.land': { + name: 'Collab.Land', + }, + 'compound.finance': { + name: 'Compound', + }, + 'cream.finance': { + name: 'Cream', + }, + 'curve.fi': { + name: 'Curve', + }, + 'defisaver.com': { + name: 'DeFi Saver', + }, + 'dydx.exchange': { + name: 'dYdX', + }, + 'ens.domains': { + name: 'ENS', + }, + 'etherscan.io': { + name: 'Etherscan', + }, + 'flexa.network': { + name: 'Flexa', + }, + 'foundation.app': { + name: 'Foundation', + }, + 'furucombo.app': { + name: 'Furucombo', + }, + 'gnosis-safe.io': { + name: 'Gnosis Safe', + }, + 'indexcoop.com': { + name: 'Index', + }, + 'instadapp.io': { + name: 'Instadapp', + }, + 'kyberswap.com': { + name: 'KyberSwap', + }, + 'matcha.xyz': { + name: 'Matcha', + }, + 'mirror.xyz': { + name: 'Mirror', + }, + 'mstable.org': { + name: 'mStable', + }, + 'mycrypto.com': { + name: 'MyCrypto', + }, + 'nft20.io': { + name: 'NFT20', + }, + 'niftygateway.com': { + name: 'Nifty Gateway', + }, + 'oasis.app': { + name: 'Oasis', + }, + 'opensea.io': { + name: 'OpenSea', + }, + 'optimism.io': { + name: 'Optimism Gateway', + }, + 'partybid.app': { + name: 'PartyBid', + }, + 'piedao.org': { + name: 'PieDAO', + }, + 'pooltogether.com': { + name: 'PoolTogether', + }, + 'punks.house': { + name: 'Punk House', + }, + 'quickswap.exchange': { + name: 'QuickSwap', + }, + 'rainbowkit.com': { + name: 'RainbowKit', + }, + 'rarible.com': { + name: 'Rarible', + }, + 'snapshot.org': { + name: 'Snapshot', + }, + 'superrare.com': { + name: 'SuperRare', + }, + 'sushi.com': { + name: 'Sushi', + }, + 'swerve.fi': { + name: 'Swerve', + }, + 'synthetix.exchange': { + name: 'Synthetix', + }, + 'tokensets.com': { + name: 'TokenSets', + }, + 'twitter.com': { + name: 'Twitter', + }, + 'umaproject.org': { + name: 'UMA', + }, + 'unisocks.exchange': { + name: 'Unisocks Exchange', + }, + 'uniswap.org': { + name: 'Uniswap', + }, + 'walletconnect.org': { + name: 'WalletConnect', + }, + 'yam.finance': { + name: 'YAM', + }, + 'yearn.finance': { + name: 'yearn', + }, + 'zapper.fi': { + name: 'Zapper', + }, + 'zerion.io': { + name: 'Zerion', + }, + 'zora.co': { + name: 'Zora', + }, + 'base.org': { + name: 'Base', + }, + 'zora.energy': { + name: 'Zora Energy', + }, +}; + +export const getHardcodedDappInformation = (hostName: string) => + displayDappNames?.[hostName]; diff --git a/src/walletConnect/index.tsx b/src/walletConnect/index.tsx index b5d3c797643..1d082711127 100644 --- a/src/walletConnect/index.tsx +++ b/src/walletConnect/index.tsx @@ -53,6 +53,8 @@ import { AuthRequest } from '@/walletConnect/sheets/AuthRequest'; import { getProviderForNetwork } from '@/handlers/web3'; import { RainbowNetworks } from '@/networks'; import { uniq } from 'lodash'; +import { fetchDappMetadata } from '@/resources/metadata/dapp'; +import { DAppStatus } from '@/graphql/__generated__/metadata'; const SUPPORTED_EVM_CHAIN_IDS = RainbowNetworks.filter( ({ features }) => features.walletconnect @@ -458,6 +460,7 @@ export async function onSessionProposal( logger.DebugContext.walletconnect ); + const verifiedData = proposal.verifyContext.verified; const receivedTimestamp = Date.now(); const { proposer, @@ -488,6 +491,7 @@ export async function onSessionProposal( peerId: proposer.publicKey, isWalletConnectV2: true, }, + verifiedData, timedOut: false, callback: async (approved, approvedChainId, accountAddress) => { const client = await web3WalletClient; @@ -1002,13 +1006,22 @@ export async function onAuthRequest(event: Web3WalletTypes.AuthRequest) { } }; + // need to prefetch dapp metadata since portal is static + const url = + // @ts-ignore Web3WalletTypes.AuthRequest type is missing VerifyContext + event?.verifyContext?.verifyUrl || event.params.requester.metadata.url; + const metadata = await fetchDappMetadata({ url, status: true }); + + const isScam = metadata.status === DAppStatus.Scam; portal.open( () => AuthRequest({ authenticate, requesterMeta: event.params.requester.metadata, + // @ts-ignore Web3WalletTypes.AuthRequest type is missing VerifyContext + verifiedData: event?.verifyContext, }), - { sheetHeight: IS_ANDROID ? 560 : 520 } + { sheetHeight: IS_ANDROID ? 560 : 520 + (isScam ? 40 : 0) } ); } diff --git a/src/walletConnect/sheets/AuthRequest.tsx b/src/walletConnect/sheets/AuthRequest.tsx index df2eb7592da..1b8a001f68c 100644 --- a/src/walletConnect/sheets/AuthRequest.tsx +++ b/src/walletConnect/sheets/AuthRequest.tsx @@ -24,13 +24,19 @@ import { getAccountProfileInfo } from '@/helpers/accountInfo'; import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; +import { Verify } from '@walletconnect/types'; +import { useDappMetadata } from '@/resources/metadata/dapp'; +import { DAppStatus } from '@/graphql/__generated__/metadata'; +import { InfoAlert } from '@/components/info-alert/info-alert'; export function AuthRequest({ requesterMeta, authenticate, + verifiedData, }: { requesterMeta: Web3WalletTypes.AuthRequest['params']['requester']['metadata']; authenticate: AuthRequestAuthenticateSignature; + verifiedData?: Verify.Context['verified']; }) { const { accountAddress } = useSelector((state: AppState) => ({ accountAddress: state.settings.accountAddress, @@ -93,6 +99,13 @@ export function AuthRequest({ const { icons, name, url } = requesterMeta; + const dappUrl = verifiedData?.verifyUrl || url; + const { data: metadata } = useDappMetadata({ url: dappUrl }); + + const isScam = metadata?.status === DAppStatus.Scam; + + const accentColor = isScam ? 'red' : 'blue'; + return ( <> @@ -101,36 +114,47 @@ export function AuthRequest({ {lang.t(lang.l.walletconnect.auth.signin_title)} - - - - {icons[0] && !loadError ? ( - setLoadError(true)} - source={{ - uri: icons[0], - }} - size={100} - height={{ custom: 54 }} - width={{ custom: 54 }} - borderRadius={14} - /> - ) : ( - - {initials(name)} - + + + {({ backgroundColor }) => ( + + + {icons[0] && !loadError ? ( + setLoadError(true)} + source={{ + uri: icons[0], + }} + size={100} + height={{ custom: 54 }} + width={{ custom: 54 }} + borderRadius={14} + /> + ) : ( + + {initials(name)} + + )} + + )} - - + + - + {url} - - + { navigate(Routes.CHANGE_WALLET_SHEET, { @@ -231,12 +259,32 @@ export function AuthRequest({ - - - + {!isScam && ( + + + + )} + + {isScam && ( + + + 􀘰 + + } + title={lang.t( + lang.l.walletconnect.dapp_warnings.info_alert.title + )} + description={lang.t( + lang.l.walletconnect.dapp_warnings.info_alert.description + )} + /> + + )} - + {({ backgroundColor }) => ( =0.10.0, whatwg-fetch@^3.0.0: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz#caefd92ae630b91c07345537e67f8354db470973" integrity sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw== +whatwg-url-without-unicode@8.0.0-3: + version "8.0.0-3" + resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b" + integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig== + dependencies: + buffer "^5.4.3" + punycode "^2.1.1" + webidl-conversions "^5.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"