diff --git a/app/component-library/components/Icons/Icon/scripts/generate-assets.js b/app/component-library/components/Icons/Icon/scripts/generate-assets.js index 4c1fb6d9d68..8bc0df50588 100644 --- a/app/component-library/components/Icons/Icon/scripts/generate-assets.js +++ b/app/component-library/components/Icons/Icon/scripts/generate-assets.js @@ -2,7 +2,7 @@ /* eslint-disable import/no-commonjs, import/no-nodejs-modules, import/no-nodejs-modules, no-console */ const fs = require('fs'); const path = require('path'); - +const { regex } = require('../../../../../../app/util/regex'); const ASSETS_FOLDER = 'assets'; const GENERATED_ASSETS_FILE = 'Icon.assets.ts'; const TYPES_FILE = 'Icon.types.ts'; @@ -33,7 +33,10 @@ const main = async () => { assetFileList.forEach((fileName) => { const filePath = path.join(__dirname, `../${ASSETS_FOLDER}/${fileName}`); const fileContent = fs.readFileSync(filePath, { encoding: 'utf-8' }); - const formattedFileContent = fileContent.replace(/black/g, 'currentColor'); + const formattedFileContent = fileContent.replace( + regex.colorBlack, + 'currentColor', + ); fs.writeFileSync(filePath, formattedFileContent); }); diff --git a/app/components/Base/Keypad/createKeypadRule.js b/app/components/Base/Keypad/createKeypadRule.js index c08d1ea3a05..882329ff3f0 100644 --- a/app/components/Base/Keypad/createKeypadRule.js +++ b/app/components/Base/Keypad/createKeypadRule.js @@ -1,10 +1,6 @@ +import { regex, hasDecimals } from '../../../../app/util/regex'; import { KEYS } from './constants'; -const hasOneDigit = /^\d$/; -function hasDecimals(separator, decimalPlaces) { - return new RegExp(`^\\d+\\${separator}\\d{${decimalPlaces}}$`, 'g'); -} - export default function createKeypadRule({ decimalSeparator = null, decimals = null, @@ -30,7 +26,7 @@ export default function createKeypadRule({ if (currentAmount === '0') { return currentAmount; } - if (hasOneDigit.test(currentAmount)) { + if (regex.hasOneDigit.test(currentAmount)) { return '0'; } diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index 59cedfc7497..88f04cc77a8 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -45,6 +45,7 @@ import { selectSelectedAddress, } from '../../../selectors/preferencesController'; import { createAccountSelectorNavDetails } from '../../Views/AccountSelector'; +import { regex } from '../../../../app/util/regex'; const createStyles = (colors) => StyleSheet.create({ @@ -315,7 +316,7 @@ class AccountOverview extends PureComponent { onOpenPortfolio = () => { const { navigation, browserTabs } = this.props; const existingPortfolioTab = browserTabs.find((tab) => - tab.url.match(new RegExp(`${AppConstants.PORTFOLIO_URL}/(?![a-z])`)), + tab.url.match(regex.portfolioUrl), ); let existingTabId; let newTabUrl; diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 7dcacfdb083..94448a6211d 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -8,6 +8,7 @@ import { useAccounts } from '../../../components/hooks/useAccounts'; import { View } from 'react-native'; import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds'; import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { regex } from '../../../../app/util/regex'; const mockEngine = Engine; @@ -136,15 +137,17 @@ describe('AccountSelectorList', () => { `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, ); - expect(within(businessAccountItem).getByText(/1 ETH/)).toBeDefined(); - expect(within(businessAccountItem).getByText(/\$3200/)).toBeDefined(); + expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); + expect( + within(businessAccountItem).getByText(regex.usd(3200)), + ).toBeDefined(); - expect(within(personalAccountItem).getByText(/2 ETH/)).toBeDefined(); - expect(within(personalAccountItem).getByText(/\$6400/)).toBeDefined(); + expect(within(personalAccountItem).getByText(regex.eth(2))).toBeDefined(); + expect( + within(personalAccountItem).getByText(regex.usd(6400)), + ).toBeDefined(); - const accounts = getAllByTestId( - new RegExp(`${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}`), - ); + const accounts = getAllByTestId(regex.accountBalance); expect(accounts.length).toBe(2); expect(toJSON()).toMatchSnapshot(); @@ -166,17 +169,17 @@ describe('AccountSelectorList', () => { }); await waitFor(async () => { - const accounts = getAllByTestId( - new RegExp(`${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}`), - ); + const accounts = getAllByTestId(regex.accountBalance); expect(accounts.length).toBe(1); const businessAccountItem = await queryByTestId( `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); - expect(within(businessAccountItem).getByText(/1 ETH/)).toBeDefined(); - expect(within(businessAccountItem).getByText(/\$3200/)).toBeDefined(); + expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); + expect( + within(businessAccountItem).getByText(regex.usd(3200)), + ).toBeDefined(); expect(toJSON()).toMatchSnapshot(); }); diff --git a/app/components/UI/AddCustomToken/index.js b/app/components/UI/AddCustomToken/index.js index 2deccd00066..ca79a396b73 100644 --- a/app/components/UI/AddCustomToken/index.js +++ b/app/components/UI/AddCustomToken/index.js @@ -33,6 +33,7 @@ import { TOKEN_PRECISION_WARNING_MESSAGE_ID, } from '../../../../wdio/screen-objects/testIDs/Screens/AddCustomToken.testIds'; import { NFT_IDENTIFIER_INPUT_BOX_ID } from '../../../../wdio/screen-objects/testIDs/Screens/NFTImportScreen.testIds'; +import { regex } from '../../../../app/util/regex'; const createStyles = (colors) => StyleSheet.create({ @@ -190,7 +191,7 @@ export default class AddCustomToken extends PureComponent { const { chainId } = this.props; const toSmartContract = isValidTokenAddress && (await isSmartContractAddress(address, chainId)); - const addressWithoutSpaces = address.replace(/\s/g, ''); + const addressWithoutSpaces = address.replace(regex.addressWithSpaces, ''); if (addressWithoutSpaces.length === 0) { this.setState({ warningAddress: strings('token.address_cant_be_empty') }); validated = false; @@ -211,7 +212,7 @@ export default class AddCustomToken extends PureComponent { validateCustomTokenSymbol = () => { let validated = true; const symbol = this.state.symbol; - const symbolWithoutSpaces = symbol.replace(/\s/g, ''); + const symbolWithoutSpaces = symbol.replace(regex.addressWithSpaces, ''); if (symbolWithoutSpaces.length === 0) { this.setState({ warningSymbol: strings('token.symbol_cant_be_empty') }); validated = false; @@ -224,7 +225,7 @@ export default class AddCustomToken extends PureComponent { validateCustomTokenDecimals = () => { let validated = true; const decimals = this.state.decimals; - const decimalsWithoutSpaces = decimals.replace(/\s/g, ''); + const decimalsWithoutSpaces = decimals.replace(regex.addressWithSpaces, ''); if (decimalsWithoutSpaces.length === 0) { this.setState({ warningDecimals: strings('token.decimals_cant_be_empty'), diff --git a/app/components/UI/ApproveTransactionReview/index.js b/app/components/UI/ApproveTransactionReview/index.js index be2bbf1b4ee..65296adab77 100644 --- a/app/components/UI/ApproveTransactionReview/index.js +++ b/app/components/UI/ApproveTransactionReview/index.js @@ -98,6 +98,7 @@ import { getRampNetworks } from '../../../reducers/fiatOrders'; import SkeletonText from '../Ramp/components/SkeletonText'; import InfoModal from '../../../components/UI/Swaps/components/InfoModal'; import BlockaidBanner from '../BlockaidBanner/BlockaidBanner'; +import { regex } from '../../../../app/util/regex'; const { ORIGIN_DEEPLINK, ORIGIN_QR_CODE } = AppConstants.DEEPLINKS; const POLLING_INTERVAL_ESTIMATED_L1_FEE = 30000; @@ -684,7 +685,7 @@ class ApproveTransactionReview extends PureComponent { handleCustomSpendOnInputChange = (value) => { if (isNumber(value)) { this.setState({ - tokenSpendValue: value.replace(/[^0-9.]/g, ''), + tokenSpendValue: value.replace(regex.nonNumber, ''), }); } }; diff --git a/app/components/UI/BrowserUrlBar/BrowserUrlBar.tsx b/app/components/UI/BrowserUrlBar/BrowserUrlBar.tsx index 496916eb3c3..103b329f36c 100644 --- a/app/components/UI/BrowserUrlBar/BrowserUrlBar.tsx +++ b/app/components/UI/BrowserUrlBar/BrowserUrlBar.tsx @@ -17,6 +17,7 @@ import stylesheet from './BrowserUrlBar.styles'; import generateTestId from '../../../../wdio/utils/generateTestId'; import { NAVBAR_TITLE_NETWORK } from '../../../../wdio/screen-objects/testIDs/BrowserScreen/BrowserScreen.testIds'; import Url from 'url-parse'; +import { regex } from '../../../../app/util/regex'; const BrowserUrlBar = ({ url, route, onPress }: BrowserUrlBarProps) => { const getDappMainUrl = () => { @@ -30,9 +31,9 @@ const BrowserUrlBar = ({ url, route, onPress }: BrowserUrlBarProps) => { url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1 && Boolean(ensUrl) ) { - return ensUrl.toLowerCase().replace(/^www\./, ''); + return ensUrl.toLowerCase().replace(regex.startUrl, ''); } - return urlObj.host.toLowerCase().replace(/^www\./, ''); + return urlObj.host.toLowerCase().replace(regex.startUrl, ''); }; const contentProtocol = getURLProtocol(url); diff --git a/app/components/UI/Ramp/Views/Settings/AddActivationKey.tsx b/app/components/UI/Ramp/Views/Settings/AddActivationKey.tsx index 2d22a1d9170..09a6c23b46a 100644 --- a/app/components/UI/Ramp/Views/Settings/AddActivationKey.tsx +++ b/app/components/UI/Ramp/Views/Settings/AddActivationKey.tsx @@ -15,11 +15,9 @@ import { import { useTheme } from '../../../../../util/theme'; import Routes from '../../../../../constants/navigation/Routes'; import { strings } from '../../../../../../locales/i18n'; - +import { regex } from '../../../../../../app/util/regex'; import styles from './Settings.styles'; -const activationKeyRegex = /^[a-zA-Z0-9\\-]{1,32}$/; - interface AddActivationKeyParams { onSubmit: (key: string) => void; } @@ -50,7 +48,7 @@ function AddActivationKey() { }, [colors, navigation]); const handleSubmit = useCallback(() => { - if (!activationKeyRegex.test(newKey)) { + if (!regex.activationKey.test(newKey)) { return; } onSubmit(newKey); @@ -95,7 +93,7 @@ function AddActivationKey() { diff --git a/app/components/UI/Tokens/index.tsx b/app/components/UI/Tokens/index.tsx index b2d529a8eed..61adfbaf8db 100644 --- a/app/components/UI/Tokens/index.tsx +++ b/app/components/UI/Tokens/index.tsx @@ -90,6 +90,7 @@ import { import { selectDetectedTokens } from '../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; import { selectUseTokenDetection } from '../../../selectors/preferencesController'; +import { regex } from '../../../../app/util/regex'; const Tokens: React.FC = ({ tokens }) => { const { colors } = useTheme(); @@ -415,7 +416,7 @@ const Tokens: React.FC = ({ tokens }) => { const onOpenPortfolio = () => { const existingPortfolioTab = browserTabs.find((tab: BrowserTab) => - tab.url.match(new RegExp(`${AppConstants.PORTFOLIO_URL}/(?![a-z])`)), + tab.url.match(regex.portfolioUrl), ); let existingTabId; let newTabUrl; diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index 54645a04c4c..47b183615dd 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -39,6 +39,7 @@ import { import { selectTokensByAddress } from '../../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../../selectors/tokenRatesController'; import { selectSelectedAddress } from '../../../../selectors/preferencesController'; +import { regex } from '../../../../../app/util/regex'; const createStyles = (colors) => StyleSheet.create({ @@ -362,7 +363,7 @@ class TransactionDetails extends PureComponent { {!!transaction?.nonce && ( {`#${parseInt( - transaction.nonce.replace(/^#/, ''), + transaction.nonce.replace(regex.transactionNonce, ''), 16, )}`} )} diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index d01cb188991..1804d990a55 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -89,6 +89,7 @@ import { selectSelectedAddress, } from '../../../selectors/preferencesController'; import { IPFS_GATEWAY_DISABLED_ERROR } from './constants'; +import { regex } from '../../../../app/util/regex'; const { HOMEPAGE_URL, NOTIFICATION_NAMES } = AppConstants; const HOMEPAGE_HOST = new URL(HOMEPAGE_URL)?.hostname; @@ -799,7 +800,7 @@ export const BrowserTab = (props) => { // Stops normal loading when it's ens, instead call go to be properly set up if (isENSUrl(url)) { - go(url.replace(/^http:\/\//, 'https://')); + go(url.replace(regex.urlHttpToHttps, 'https://')); return false; } diff --git a/app/components/Views/ChoosePassword/index.js b/app/components/Views/ChoosePassword/index.js index 79ff561aeb9..1c06c09872b 100644 --- a/app/components/Views/ChoosePassword/index.js +++ b/app/components/Views/ChoosePassword/index.js @@ -70,6 +70,7 @@ import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; import generateTestId from '../../../../wdio/utils/generateTestId'; import { scale } from 'react-native-size-matters'; import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse'; +import { regex } from '../../../util/regex'; const createStyles = (colors) => StyleSheet.create({ @@ -554,7 +555,7 @@ class ChoosePassword extends PureComponent { const mnemonic = await KeyringController.exportSeedPhrase( keychainPassword, ).toString(); - return JSON.stringify(mnemonic).replace(/"/g, ''); + return JSON.stringify(mnemonic).replace(regex.privateCredentials, ''); }; jumpToConfirmPassword = () => { diff --git a/app/components/Views/ManualBackupStep1/index.js b/app/components/Views/ManualBackupStep1/index.js index 743c27ab787..1804886acc5 100644 --- a/app/components/Views/ManualBackupStep1/index.js +++ b/app/components/Views/ManualBackupStep1/index.js @@ -36,6 +36,7 @@ import { CONFIRM_CHANGE_PASSWORD_INPUT_BOX_ID } from '../../../constants/test-id import { MetaMetricsEvents } from '../../../core/Analytics'; import AnalyticsV2 from '../../../util/analyticsV2'; import { Authentication } from '../../../core'; +import { regex } from '../../../../app/util/regex'; /** * View that's shown during the second step of @@ -66,7 +67,9 @@ const ManualBackupStep1 = ({ route, navigation, appTheme }) => { const mnemonic = await KeyringController.exportSeedPhrase( password, ).toString(); - return JSON.stringify(mnemonic).replace(/"/g, '').split(' '); + return JSON.stringify(mnemonic) + .replace(regex.privateCredentials, '') + .split(' '); }; useEffect(() => { diff --git a/app/components/Views/ResetPassword/index.js b/app/components/Views/ResetPassword/index.js index c276fe4a205..e97b0a29624 100644 --- a/app/components/Views/ResetPassword/index.js +++ b/app/components/Views/ResetPassword/index.js @@ -63,6 +63,7 @@ import { recreateVaultWithNewPassword } from '../../../core/Vault'; import generateTestId from '../../../../wdio/utils/generateTestId'; import Logger from '../../../util/Logger'; import { selectSelectedAddress } from '../../../selectors/preferencesController'; +import { regex } from '../../../../app/util/regex'; const createStyles = (colors) => StyleSheet.create({ @@ -473,7 +474,7 @@ class ResetPassword extends PureComponent { const mnemonic = await KeyringController.exportSeedPhrase( keychainPassword, ).toString(); - return JSON.stringify(mnemonic).replace(/"/g, ''); + return JSON.stringify(mnemonic).replace(regex.privateCredentials, ''); }; jumpToConfirmPassword = () => { diff --git a/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx b/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx index ff878c4fa54..f7c14fcd3ed 100644 --- a/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx +++ b/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx @@ -58,6 +58,7 @@ import { SECRET_RECOVERY_PHRASE_TEXT, } from '../../../../wdio/screen-objects/testIDs/Screens/RevelSecretRecoveryPhrase.testIds'; import { selectSelectedAddress } from '../../../selectors/preferencesController'; +import { regex } from '../../../../app/util/regex'; const PRIVATE_KEY = 'private_key'; @@ -126,7 +127,10 @@ const RevealPrivateCredential = ({ const mnemonic = await KeyringController.exportSeedPhrase( pswd, ).toString(); - privateCredential = JSON.stringify(mnemonic).replace(/"/g, ''); + privateCredential = JSON.stringify(mnemonic).replace( + regex.privateCredentials, + '', + ); } else { privateCredential = await KeyringController.exportAccount( pswd, diff --git a/app/components/Views/SendFlow/AddressList/AddressList.tsx b/app/components/Views/SendFlow/AddressList/AddressList.tsx index 0cfe876e687..b9693a38dd9 100644 --- a/app/components/Views/SendFlow/AddressList/AddressList.tsx +++ b/app/components/Views/SendFlow/AddressList/AddressList.tsx @@ -17,6 +17,7 @@ import Text from '../../../../component-library/components/Texts/Text/Text'; import { TextVariant } from '../../../../component-library/components/Texts/Text'; import { selectChainId } from '../../../../selectors/networkController'; import { selectIdentities } from '../../../../selectors/preferencesController'; +import { regex } from '../../../../../app/util/regex'; // Internal dependencies import { AddressListProps, Contact } from './AddressList.types'; @@ -81,7 +82,7 @@ const AddressList: React.FC = ({ updatedContacts.forEach((contact: Contact) => { const contactNameInitial = contact?.name?.[0]; - const nameInitial = /[a-z]/i.exec(contactNameInitial); + const nameInitial = regex.nameInitial.exec(contactNameInitial); const initial = nameInitial ? nameInitial[0].toLowerCase() : strings('address_book.others'); diff --git a/app/components/Views/SendFlow/Amount/index.js b/app/components/Views/SendFlow/Amount/index.js index cd66328afbb..ff148b1061c 100644 --- a/app/components/Views/SendFlow/Amount/index.js +++ b/app/components/Views/SendFlow/Amount/index.js @@ -103,6 +103,7 @@ import { selectContractBalances } from '../../../../selectors/tokenBalancesContr import { selectSelectedAddress } from '../../../../selectors/preferencesController'; import { PREFIX_HEX_STRING } from '../../../../constants/transaction'; import Routes from '../../../../constants/navigation/Routes'; +import { regex } from '../../../../../app/util/regex'; const KEYBOARD_OFFSET = Device.isSmallDevice() ? 80 : 120; @@ -890,7 +891,7 @@ class Amount extends PureComponent { hasExchangeRate, comma; // Remove spaces from input - inputValue = inputValue && inputValue.replace(/\s+/g, ''); + inputValue = inputValue && inputValue.replace(regex.whiteSpaces, ''); // Handle semicolon for other languages if (inputValue && inputValue.includes(',')) { comma = true; diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index ffb403e7363..ecedc126155 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -83,6 +83,7 @@ import { selectNetworkConfigurations, selectProviderConfig, } from '../../../../../selectors/networkController'; +import { regex } from '../../../../../../app/util/regex'; const createStyles = (colors) => StyleSheet.create({ @@ -654,12 +655,12 @@ class NetworkSettings extends PureComponent { // Check if it's a valid chainId format if (chainId.startsWith('0x')) { - if (!/^0x[0-9a-f]+$/iu.test(chainId)) { + if (!regex.validChainId_hex.test(chainId)) { errorMessage = strings('app_settings.invalid_hex_number'); } else if (!isPrefixedFormattedHexString(chainId)) { errorMessage = strings('app_settings.invalid_hex_number_leading_zeros'); } - } else if (!/^[0-9]+$/u.test(chainId)) { + } else if (!regex.validChainId.test(chainId)) { errorMessage = strings('app_settings.invalid_number'); } else if (chainId.startsWith('0')) { errorMessage = strings('app_settings.invalid_number_leading_zeros'); diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index b6ac1334222..a08767c6e5d 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -38,6 +38,7 @@ import { selectProviderConfig, selectProviderType, } from '../../selectors/networkController'; +import { regex } from '../../../app/util/regex'; const Engine = ImportedEngine as any; @@ -708,13 +709,9 @@ export const getRpcMethodMiddleware = ({ checkTabActive(); navigation.navigate('QRScanner', { onScanSuccess: (data: any) => { - const regex = new RegExp(req.params[0]); - if (regex && !regex.exec(data)) { + if (!regex.exec(req.params[0], data)) { reject({ message: 'NO_REGEX_MATCH', data }); - } else if ( - !regex && - !/^(0x){1}[0-9a-fA-F]{40}$/i.exec(data.target_address) - ) { + } else if (regex.walletAddress.exec(data.target_address)) { reject({ message: 'INVALID_ETHEREUM_ADDRESS', data: data.target_address, diff --git a/app/store/migrations.js b/app/store/migrations.js index 4dad5088c1b..1b0b40ad3cd 100644 --- a/app/store/migrations.js +++ b/app/store/migrations.js @@ -15,6 +15,7 @@ import { EXPLORED, } from '../constants/storage'; import { GOERLI, IPFS_DEFAULT_GATEWAY_URL } from '../../app/constants/network'; +import { regex } from '../../app/util/regex'; // Generated using this script: https://gist.github.com/Gudahtt/7a8a9e452bd2efdc5ceecd93610a25d3 import ambiguousNetworks from './migration-data/amibiguous-networks.json'; @@ -90,7 +91,7 @@ export const migrations = { // If provider is rpc, check if the current network has a valid chainId const storedChainId = typeof provider.chainId === 'string' ? provider.chainId : ''; - const isDecimalString = /^[1-9]\d*$/u.test(storedChainId); + const isDecimalString = regex.decimalStringMigrations.test(storedChainId); const hasInvalidChainId = !isDecimalString || !isSafeChainId(parseInt(storedChainId, 10)); diff --git a/app/util/ENSUtils.js b/app/util/ENSUtils.js index 84c476393cb..83dee6472d2 100644 --- a/app/util/ENSUtils.js +++ b/app/util/ENSUtils.js @@ -11,6 +11,7 @@ const INVALID_ENS_NAME_ERROR = 'invalid ENS name'; // One hour cache threshold. const CACHE_REFRESH_THRESHOLD = 60 * 60 * 1000; import { EMPTY_ADDRESS } from '../constants/transaction'; +import { regex } from '../../app/util/regex'; /** * Utility class with the single responsibility @@ -110,5 +111,5 @@ export async function doENSLookup(ensName, chainId) { } export function isDefaultAccountName(name) { - return /^Account \d*$/.test(name); + return regex.defaultAccount.test(name); } diff --git a/app/util/address/index.js b/app/util/address/index.js index 89623dcce84..df22cd9d4f6 100644 --- a/app/util/address/index.js +++ b/app/util/address/index.js @@ -32,6 +32,7 @@ import { PROTOCOLS } from '../../constants/deeplinks'; import TransactionTypes from '../../core/TransactionTypes'; import { selectChainId } from '../../selectors/networkController'; import { store } from '../../store'; +import { regex } from '../../../app/util/regex'; const { ASSET: { ERC721, ERC1155 }, @@ -206,15 +207,10 @@ export function getAddressAccountType(address) { export function isENS(name = undefined) { if (!name) return false; - const match = punycode - .toASCII(name) - .toLowerCase() - // Checks that the domain consists of at least one valid domain pieces separated by periods, followed by a tld - // Each piece of domain name has only the characters a-z, 0-9, and a hyphen (but not at the start or end of chunk) - // A chunk has minimum length of 1, but minimum tld is set to 2 for now (no 1-character tlds exist yet) - .match( - /^(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)+[a-z0-9][-a-z0-9]*[a-z0-9]$/u, - ); + // Checks that the domain consists of at least one valid domain pieces separated by periods, followed by a tld + // Each piece of domain name has only the characters a-z, 0-9, and a hyphen (but not at the start or end of chunk) + // A chunk has minimum length of 1, but minimum tld is set to 2 for now (no 1-character tlds exist yet) + const match = punycode.toASCII(name).toLowerCase().match(regex.ensName); const OFFSET = 1; const index = name && name.lastIndexOf('.'); diff --git a/app/util/browser/index.ts b/app/util/browser/index.ts index d90a223fc90..5df234fc91e 100644 --- a/app/util/browser/index.ts +++ b/app/util/browser/index.ts @@ -1,6 +1,7 @@ import { Linking } from 'react-native'; import isUrl from 'is-url'; import Url from 'url-parse'; +import { regex, hasProtocol } from '../../util/regex'; /** * Returns URL prefixed with protocol @@ -13,8 +14,7 @@ export const prefixUrlWithProtocol = ( url: string, defaultProtocol = 'https://', ) => { - const hasProtocol = /^[a-z]*:\/\//.test(url); - const sanitizedURL = hasProtocol ? url : `${defaultProtocol}${url}`; + const sanitizedURL = hasProtocol(url) ? url : `${defaultProtocol}${url}`; return sanitizedURL; }; @@ -33,10 +33,7 @@ export default function onUrlSubmit( defaultProtocol = 'https://', ) { //Check if it's a url or a keyword - const regEx = new RegExp( - /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!&',;=.+]+$/g, - ); - if (!isUrl(input) && !regEx.test(input)) { + if (!isUrl(input) && !input.match(regex.url)) { // Add exception for localhost if ( !input.startsWith('http://localhost') && diff --git a/app/util/networks/index.js b/app/util/networks/index.js index 5d6d8141f55..fb61cae41d3 100644 --- a/app/util/networks/index.js +++ b/app/util/networks/index.js @@ -25,6 +25,7 @@ import { toLowerCaseEquals } from '../general'; import { fastSplit } from '../number'; import { buildUnserializedTransaction } from '../transactions/optimismTransaction'; import handleNetworkSwitch from './handleNetworkSwitch'; +import { regex } from '../../../app/util/regex'; export { handleNetworkSwitch }; @@ -231,12 +232,7 @@ export function hasBlockExplorer(key) { } export function isprivateConnection(hostname) { - return ( - hostname === 'localhost' || - /(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/.test( - hostname, - ) - ); + return hostname === 'localhost' || regex.localNetwork.test(hostname); } /** @@ -321,7 +317,7 @@ export function isPrefixedFormattedHexString(value) { if (typeof value !== 'string') { return false; } - return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value); + return regex.prefixedFormattedHexString.test(value); } export const getNetworkNonce = async ({ from }) => { diff --git a/app/util/number/index.js b/app/util/number/index.js index 7d911636af1..30f1ce59734 100644 --- a/app/util/number/index.js +++ b/app/util/number/index.js @@ -13,6 +13,7 @@ import BigNumber from 'bignumber.js'; import currencySymbols from '../currency-symbols.json'; import { isZero } from '../lodash'; +import { regex } from '../regex'; export { BNToHex }; // Big Number Constants @@ -64,11 +65,11 @@ const baseChange = { * @returns {string} The prefixed string. */ export const addHexPrefix = (str) => { - if (typeof str !== 'string' || str.match(/^-?0x/u)) { + if (typeof str !== 'string' || str.match(regex.hexPrefix)) { return str; } - if (str.match(/^-?0X/u)) { + if (str.match(regex.hexPrefix)) { return str.replace('0X', '0x'); } @@ -110,7 +111,7 @@ export function fromTokenMinimalUnit(minimalInput, decimals) { while (fraction.length < decimals) { fraction = '0' + fraction; } - fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]; + fraction = fraction.match(regex.fractions)[1]; const whole = minimal.div(base).toString(10); let value = '' + whole + (fraction === '0' ? '' : '.' + fraction); if (negative) { @@ -119,9 +120,6 @@ export function fromTokenMinimalUnit(minimalInput, decimals) { return value; } -const INTEGER_REGEX = /^-?\d*(\.0+|\.)?$/; -export const INTEGER_OR_FLOAT_REGEX = /^[+-]?\d+(\.\d+)?$/; - /** * Converts token minimal unit to readable string value * @@ -135,7 +133,7 @@ export function fromTokenMinimalUnitString(minimalInput, decimals) { } const tokenFormat = ethersUtils.formatUnits(minimalInput, decimals); - const isInteger = Boolean(INTEGER_REGEX.exec(tokenFormat)); + const isInteger = Boolean(regex.integer.exec(tokenFormat)); const [integerPart, decimalPart] = tokenFormat.split('.'); if (isInteger) { @@ -373,7 +371,7 @@ export function toBN(value) { * @returns {boolean} - True if the string is a valid number */ export function isNumber(str) { - return /^(\d+(\.\d+)?)$/.test(str); + return regex.number.test(str); } export const dotAndCommaDecimalFormatter = (value) => { @@ -493,12 +491,12 @@ export function addCurrencySymbol( const decimalString = amount.toString().split('.')[1]; if (decimalString && decimalString.length > 1) { const firstNonZeroDecimal = decimalString.indexOf( - decimalString.match(/[1-9]/)[0], + decimalString.match(regex.decimalString)[0], ); if (firstNonZeroDecimal > 0) { amount = parseFloat(amount).toFixed(firstNonZeroDecimal + 3); // remove trailing zeros - amount = amount.replace(/\.?0+$/, ''); + amount = amount.replace(regex.trailingZero, ''); } } } @@ -714,7 +712,7 @@ export function isPrefixedFormattedHexString(value) { if (typeof value !== 'string') { return false; } - return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value); + return regex.prefixedFormattedHexString.test(value); } const converter = ({ diff --git a/app/util/regex/index.test.ts b/app/util/regex/index.test.ts new file mode 100644 index 00000000000..bd8f4121e56 --- /dev/null +++ b/app/util/regex/index.test.ts @@ -0,0 +1,525 @@ +import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds'; +import { regex, hasDecimals } from '.'; + +describe('REGEX :: hasDecimals', () => { + const separator = '.'; + const decimalPlaces = '2'; + it('should match a number with 2 decimal places separated by "."', () => { + expect(hasDecimals(separator, decimalPlaces).test('123.45')).toEqual(true); + }); + + it('should not match a number without decimal places', () => { + expect(hasDecimals(separator, decimalPlaces).test('123')).toEqual(false); + }); + + it('should not match a number with more than 2 decimal places', () => { + expect(hasDecimals(separator, decimalPlaces).test('123.456')).toEqual( + false, + ); + }); + + it('should not match a number with a different separator', () => { + expect(hasDecimals(separator, decimalPlaces).test('123,45')).toEqual(false); + }); +}); + +describe('REGEX :: REGEX_1_ETH', () => { + it('should match "1 ETH"', () => { + expect(regex.eth(1).test('1 ETH')).toEqual(true); + }); + + it('should not match "2 ETH"', () => { + expect(regex.eth(1).test('2 ETH')).toEqual(false); + }); +}); + +describe('REGEX :: REGEX_2ETH', () => { + it('should match "2 ETH"', () => { + expect(regex.eth(2).test('2 ETH')).toEqual(true); + }); + + it('should not match "1 ETH"', () => { + expect(regex.eth(2).test('1 ETH')).toEqual(false); + }); +}); + +describe('REGEX :: REGEX_3200_USD', () => { + it('should match "$3200"', () => { + expect(regex.usd(3200).test('$3200')).toEqual(true); + }); + + it('should not match "$6400"', () => { + expect(regex.usd(3200).test('$6400')).toEqual(false); + }); +}); + +describe('REGEX :: REGEX_6400_USD', () => { + it('should match "$6400"', () => { + expect(regex.usd(6400).test('$6400')).toEqual(true); + }); + + it('should not match "$3200"', () => { + expect(regex.usd(6400).test('$3200')).toEqual(false); + }); +}); + +describe('REGEX :: regex.accountBalance', () => { + it(`should match "${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}"`, () => { + expect( + regex.accountBalance.test(ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID), + ).toEqual(true); + }); + + it(`should not match "Account balance != ${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}"`, () => { + expect(regex.accountBalance.test('123')).toEqual(false); + }); +}); + +describe('REGEX :: regex.activationKey', () => { + it('should match "Abc12345"', () => { + expect(regex.activationKey.test('Abc12345')).toEqual(true); + }); + + it('should match "Abc-123-45"', () => { + expect(regex.activationKey.test('Abc-123-45')).toEqual(true); + }); + + it('should not match "Abc_123_45"', () => { + expect(regex.activationKey.test('Abc_123_45')).toEqual(false); + }); +}); + +describe('REGEX :: regex.addressWithSpaces', () => { + it('should match "This is an address"', () => { + expect(regex.addressWithSpaces.test('This is an address')).toEqual(true); + }); + + it('should not match "NoSpaces"', () => { + expect(regex.addressWithSpaces.test('NoSpaces')).toEqual(false); + }); +}); + +describe('REGEX :: regex.colorBlack', () => { + it('should match "This text is black"', () => { + expect(regex.colorBlack.test('This text is black')).toEqual(true); + }); + + it('should not match "This text is not black"', () => { + expect(regex.colorBlack.test('This text is not black')).toEqual(false); + }); +}); + +describe('REGEX :: regex.decimalString', () => { + it('should match "9"', () => { + expect(regex.decimalString.test('9')).toEqual(true); + }); + + it('should not match "0"', () => { + expect(regex.decimalString.test('0')).toEqual(false); + }); +}); + +describe('REGEX :: regex.decimalStringMigrations', () => { + it('should match "123"', () => { + expect(regex.decimalStringMigrations.test('123')).toEqual(true); + }); + + it('should match "456789"', () => { + expect(regex.decimalStringMigrations.test('456789')).toEqual(true); + }); + + it('should not match "0"', () => { + expect(regex.decimalStringMigrations.test('0')).toEqual(false); + }); +}); + +describe('REGEX :: regex.defaultAccount', () => { + it('should match "Account 123"', () => { + expect(regex.defaultAccount.test('Account 123')).toEqual(true); + }); +}); + +describe('REGEX :: regex.ensName', () => { + it('should match a valid ENS name', () => { + expect(regex.ensName.test('example.eth')).toEqual(true); + }); + + it('should not match an invalid ENS name', () => { + expect(regex.ensName.test('example.eth.')).toEqual(false); + }); +}); + +describe('REGEX :: regex.fractions', () => { + it('should match a fraction with non-zero numerator and denominator', () => { + expect(regex.fractions.test('123/456')).toEqual(true); + }); + + it('should match a fraction with zero numerator and non-zero denominator', () => { + expect(regex.fractions.test('0/123')).toEqual(true); + }); + + it('should match a fraction with zero numerator and zero denominator', () => { + expect(regex.fractions.test('0/0')).toEqual(true); + }); +}); + +describe('REGEX :: regex.hasOneDigit', () => { + it('should match a string with exactly one digit', () => { + expect(regex.hasOneDigit.test('5')).toEqual(true); + }); + + it('should not match a string without a digit', () => { + expect(regex.hasOneDigit.test('abc')).toEqual(false); + }); + + it('should not match a string with more than one digit', () => { + expect(regex.hasOneDigit.test('123')).toEqual(false); + }); +}); + +describe('REGEX :: regex.hexPrefix', () => { + it('should match a string with a hexadecimal value and optional negative sign and "0x" prefix', () => { + expect(regex.hexPrefix.test('-0x123abc')).toEqual(true); + }); + + it('should not match a string without a hexadecimal value or "0x" prefix', () => { + expect(regex.hexPrefix.test('abc123')).toEqual(false); + }); +}); + +describe('REGEX :: regex.integer', () => { + it('should match an integer', () => { + expect(regex.integer.test('123')).toEqual(true); + }); + + it('should match an integer with a decimal point and trailing zeros', () => { + expect(regex.integer.test('123.000')).toEqual(true); + }); + + it('should match a negative integer', () => { + expect(regex.integer.test('-456')).toEqual(true); + }); + + it('should match a negative integer with a decimal point and trailing zeros', () => { + expect(regex.integer.test('-456.000')).toEqual(true); + }); + + it('should not match a non-integer value', () => { + expect(regex.integer.test('12.34')).toEqual(false); + }); +}); + +describe('REGEX :: regex.localNetwork', () => { + it('should match a local network IP address starting with "127."', () => { + expect(regex.localNetwork.test('127.0.0.1')).toEqual(true); + }); + + it('should match a local network IP address starting with "10."', () => { + expect(regex.localNetwork.test('10.0.0.1')).toEqual(true); + }); + + it('should match a local network IP address starting with "172.16."', () => { + expect(regex.localNetwork.test('172.16.0.1')).toEqual(true); + }); + + it('should match a local network IP address starting with "172.31."', () => { + expect(regex.localNetwork.test('172.31.0.1')).toEqual(true); + }); + + it('should match a local network IP address starting with "192.168."', () => { + expect(regex.localNetwork.test('192.168.0.1')).toEqual(true); + }); + + it('should not match a non-local IP address', () => { + expect(regex.localNetwork.test('8.8.8.8')).toEqual(false); + }); +}); + +describe('REGEX :: regex.nameInitial', () => { + it('should match a single letter as a name initial', () => { + expect(regex.nameInitial.test('A')).toEqual(true); + }); + + it('should match a single letter (case insensitive) as a name initial', () => { + expect(regex.nameInitial.test('z')).toEqual(true); + }); + + it('should not match a non-letter character', () => { + expect(regex.nameInitial.test('1')).toEqual(false); + }); +}); + +describe('REGEX :: regex.nonNumber', () => { + it('should match a string containing non-digit and non-decimal point characters', () => { + expect(regex.nonNumber.test('abc$%^')).toEqual(true); + }); + + it('should not match a string containing only digits', () => { + expect(regex.nonNumber.test('123456')).toEqual(false); + }); + + it('should not match a string containing only decimal points', () => { + expect(regex.nonNumber.test('...')).toEqual(false); + }); +}); + +describe('REGEX :: regex.number', () => { + it('should match a positive integer', () => { + expect(regex.number.test('123')).toEqual(true); + }); + + it('should match a positive decimal number', () => { + expect(regex.number.test('12.34')).toEqual(true); + }); + + it('should not match a string with non-numeric characters', () => { + expect(regex.number.test('abc123')).toEqual(false); + }); + + it('should not match an empty string', () => { + expect(regex.number.test('')).toEqual(false); + }); +}); + +describe('REGEX :: regex.portfolioUrl', () => { + it('should not match empty string', () => { + expect(regex.portfolioUrl.test('')).toEqual(false); + }); + it('should empty url', () => { + expect(regex.portfolioUrl.test('http://')).toEqual(false); + }); +}); + +describe('REGEX :: regex.prefixedFormattedHexString', () => { + it('should match a formatted hexadecimal string with "0x" prefix', () => { + expect(regex.prefixedFormattedHexString.test('0x1A2B3C')).toEqual(true); + }); + + it('should match a formatted hexadecimal string with "0x" prefix and lowercase letters', () => { + expect(regex.prefixedFormattedHexString.test('0xabcdef')).toEqual(true); + }); + + it('should match a formatted hexadecimal string with "0x" prefix and trailing zeros', () => { + expect(regex.prefixedFormattedHexString.test('0x123000')).toEqual(true); + }); + + it('should not match a non-formatted hexadecimal string without "0x" prefix', () => { + expect(regex.prefixedFormattedHexString.test('123abc')).toEqual(false); + }); + + it('should not match a string with non-hexadecimal characters', () => { + expect(regex.prefixedFormattedHexString.test('0x12G34')).toEqual(false); + }); +}); + +describe('REGEX :: regex.privateCredentials', () => { + it('should match a string containing double quotation marks', () => { + expect(regex.privateCredentials.test('Hello "World"')).toEqual(true); + }); + + it('should not match a string without double quotation marks', () => { + expect(regex.privateCredentials.test('Hello World')).toEqual(false); + }); +}); + +describe('REGEX :: regex.replaceNetworkErrorSentry', () => { + it('should match a string containing a 40-character hexadecimal value with "0x" prefix', () => { + expect( + regex.replaceNetworkErrorSentry.test( + 'Error occurred at 0x1234567890ABCDEF1234567890ABCDEF12345678', + ), + ).toEqual(true); + }); + + it('should not match a string without a 40-character hexadecimal value', () => { + expect( + regex.replaceNetworkErrorSentry.test('Error occurred at 0xabcdef'), + ).toEqual(false); + }); + + it('should not match a string with a hexadecimal value without "0x" prefix', () => { + expect( + regex.replaceNetworkErrorSentry.test( + 'Error occurred at 1234567890ABCDEF1234567890ABCDEF12345678', + ), + ).toEqual(false); + }); +}); + +describe('REGEX :: regex.sanitizeUrl', () => { + it('should match a valid URL starting with "http://"', () => { + expect(regex.sanitizeUrl.test('http://www.example.com')).toEqual(true); + }); + + it('should not match an invalid URL', () => { + expect(regex.sanitizeUrl.test('invalid-url')).toEqual(false); + }); +}); + +describe('REGEX :: regex.seedPhrase', () => { + it('should match a string with one or more word characters (letters, numbers, underscore)', () => { + expect(regex.seedPhrase.test('helloWorld_123')).toEqual(true); + }); + + it('should match multiple occurrences of word characters in a string', () => { + expect(regex.seedPhrase.test('this is a seed phrase')).toEqual(true); + }); + + it('should not match a string without word characters', () => { + expect(regex.seedPhrase.test('@#$%')).toEqual(false); + }); +}); + +describe('REGEX :: regex.startUrl', () => { + it('should match a string starting with "www."', () => { + expect(regex.startUrl.test('www.example.com')).toEqual(true); + }); + + it('should not match a string without "www."', () => { + expect(regex.startUrl.test('example.com')).toEqual(false); + }); + + it('should not match a string with "www." in the middle', () => { + expect(regex.startUrl.test('hello.www.example.com')).toEqual(false); + }); +}); + +describe('REGEX :: regex.trailingSlash', () => { + it('should match a string ending with one or more slashes', () => { + expect(regex.trailingSlash.test('example.com/')).toEqual(true); + }); + + it('should not match a string without a trailing slash', () => { + expect(regex.trailingSlash.test('example.com')).toEqual(false); + }); + + it('should not match a string with slashes in the middle', () => { + expect(regex.trailingSlash.test('example.com/path/to/resource')).toEqual( + false, + ); + }); +}); + +describe('REGEX :: regex.trailingZero', () => { + it('should match a string ending with one or more zeros', () => { + expect(regex.trailingZero.test('10.5000')).toEqual(true); + }); + + it('should match a string ending with a decimal point followed by one or more zeros', () => { + expect(regex.trailingZero.test('10.0')).toEqual(true); + }); + + it('should not match a string without trailing zeros', () => { + expect(regex.trailingZero.test('10.5')).toEqual(false); + }); + + it('should not match a string with non-zero characters after the decimal point', () => { + expect(regex.trailingZero.test('10.5001')).toEqual(false); + }); +}); + +describe('REGEX :: regex.transactionNonce', () => { + it('should match a string starting with a pound sign', () => { + expect(regex.transactionNonce.test('#123')).toEqual(true); + }); + + it('should not match a string without a pound sign', () => { + expect(regex.transactionNonce.test('123')).toEqual(false); + }); + + it('should not match a string with a pound sign in the middle', () => { + expect(regex.transactionNonce.test('hello#123')).toEqual(false); + }); +}); + +describe('REGEX :: regex.url', () => { + it('should match a valid URL starting with "http://"', () => { + expect(regex.url.test('http://www.example.com')).toEqual(true); + }); + + it('should not match an invalid URL', () => { + expect(regex.url.test('invalid-url')).toEqual(false); + }); +}); + +describe('REGEX :: regex.urlHttpToHttps', () => { + it('should match a string starting with "http://"', () => { + expect(regex.urlHttpToHttps.test('http://www.example.com')).toEqual(true); + }); + + it('should not match a string starting with "https://"', () => { + expect(regex.urlHttpToHttps.test('https://www.example.com')).toEqual(false); + }); + + it('should not match a string without "http://"', () => { + expect(regex.urlHttpToHttps.test('www.example.com')).toEqual(false); + }); +}); + +describe('REGEX :: regex.validChainId', () => { + it('should match a string consisting of digits', () => { + expect(regex.validChainId.test('12345')).toEqual(true); + }); + + it('should not match a string with non-digit characters', () => { + expect(regex.validChainId.test('abc123')).toEqual(false); + }); +}); + +describe('REGEX :: regex.validChainIdHex', () => { + it('should match a string starting with "0x" followed by hex digits', () => { + expect(regex.validChainIdHex.test('0xabcdef')).toEqual(true); + }); + + it('should match a string starting with "0x" followed by uppercase hex digits', () => { + expect(regex.validChainIdHex.test('0xABCDEF')).toEqual(true); + }); + + it('should not match a string without "0x"', () => { + expect(regex.validChainIdHex.test('abcdef')).toEqual(false); + }); + + it('should not match a string with non-hex characters', () => { + expect(regex.validChainIdHex.test('0xg1h2i3j')).toEqual(false); + }); +}); + +describe('REGEX :: regex.walletAddress', () => { + it('should match a string starting with "0x" followed by 40 hex characters', () => { + expect( + regex.walletAddress.test('0x1234567890abcdefABCDEF1234567890abcdefad'), + ).toEqual(true); + }); + + it('should match a string starting with "0x" followed by 40 uppercase hex characters', () => { + expect( + regex.walletAddress.test('0xABCDEF1234567890ABCDEF1234567890ABCDEFAD'), + ).toEqual(true); + }); + + it('should not match a string without "0x"', () => { + expect( + regex.walletAddress.test('1234567890abcdefABCDEF1234567890abcdef'), + ).toEqual(false); + }); + + it('should not match a string with non-hex characters', () => { + expect( + regex.walletAddress.test('0xg1h2i3j4k5l6m7n8o9p0q1r2s3t4u5v6w7x8y9z'), + ).toEqual(false); + }); +}); + +describe('REGEX :: regex.whiteSpaces', () => { + it('should match a string with one or more white spaces', () => { + expect(regex.whiteSpaces.test('Hello World')).toEqual(true); + }); + + it('should not match a string without white spaces', () => { + expect(regex.whiteSpaces.test('HelloWorld')).toEqual(false); + }); + + it('should match multiple occurrences of white spaces', () => { + expect(regex.whiteSpaces.test('Hello World')).toEqual(true); + }); +}); diff --git a/app/util/regex/index.ts b/app/util/regex/index.ts new file mode 100644 index 00000000000..c8ea480b955 --- /dev/null +++ b/app/util/regex/index.ts @@ -0,0 +1,58 @@ +import AppConstants from '../../core/AppConstants'; +import { RegexTypes } from './index.types'; +import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds'; + +export function hasDecimals(separator: string, decimalPlaces: string) { + return new RegExp(`^\\d+\\${separator}\\d{${decimalPlaces}}$`, 'g'); +} + +export function hasProtocol(url: string) { + return /^[a-z]*:\/\//.test(url); +} + +export const regex: RegexTypes = { + eth: (num: number) => new RegExp(`${num} ETH`), + usd: (num: number) => new RegExp(`${num}`), + accountBalance: new RegExp(`${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}`), + activationKey: /^[a-zA-Z0-9\\-]{1,32}$/, + addressWithSpaces: /\s/g, + colorBlack: /black/g, + decimalString: /[1-9]/, + decimalStringMigrations: /^[1-9]\d*$/u, + defaultAccount: /^Account \d*$/, + exec: (exp: string, input: string) => new RegExp(exp).exec(input), + // Checks that the domain consists of at least one valid domain pieces separated by periods, followed by a tld + // Each piece of domain name has only the characters a-z, 0-9, and a hyphen (but not at the start or end of chunk) + // A chunk has minimum length of 1, but minimum tld is set to 2 for now (no 1-character tlds exist yet) + ensName: + /^(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)+[a-z0-9][-a-z0-9]*[a-z0-9]$/u, + fractions: /^([0-9]*[1-9]|0)(0*)/, + hasOneDigit: /^\d$/, + hexPrefix: /^-?0x/u, + integer: /^-?\d*(\.0+|\.)?$/, + localNetwork: + /(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/, + nameInitial: /[a-z]/i, + nonNumber: /[^0-9.]/g, + number: /^(\d+(\.\d+)?)$/, + portfolioUrl: new RegExp(`${AppConstants.PORTFOLIO_URL}/(?![a-z])`), + prefixedFormattedHexString: /^0x[1-9a-f]+[0-9a-f]*$/iu, + privateCredentials: /"/g, + protocol: /^[a-z]*:\/\//, + replaceNetworkErrorSentry: /0x[A-Fa-f0-9]{40}/u, + sanitizeUrl: + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gu, + seedPhrase: /\w+/gu, + startUrl: /^www\./, + trailingSlash: /\/+$/, + trailingZero: /\.?0+$/, + transactionNonce: /^#/, + url: new RegExp( + /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!&',,=.+]+$/g, + ), + urlHttpToHttps: /^http:\/\//, + validChainId: /^[0-9]+$/u, + validChainIdHex: /^0x[0-9a-f]+$/iu, + walletAddress: /^(0x){1}[0-9a-fA-F]{40}$/i, + whiteSpaces: /\s+/g, +}; diff --git a/app/util/regex/index.types.ts b/app/util/regex/index.types.ts new file mode 100644 index 00000000000..a3364c3051e --- /dev/null +++ b/app/util/regex/index.types.ts @@ -0,0 +1,38 @@ +export interface RegexTypes { + eth: (num: number) => RegExp; + usd: (num: number) => RegExp; + accountBalance: RegExp; + activationKey: RegExp; + addressWithSpaces: RegExp; + colorBlack: RegExp; + decimalStringMigrations: RegExp; + decimalString: RegExp; + defaultAccount: RegExp; + ensName: RegExp; + exec: (exp: string, input: string) => RegExpExecArray | null; + fractions: RegExp; + hasOneDigit: RegExp; + hexPrefix: RegExp; + integer: RegExp; + localNetwork: RegExp; + nameInitial: RegExp; + nonNumber: RegExp; + number: RegExp; + portfolioUrl: RegExp; + prefixedFormattedHexString: RegExp; + privateCredentials: RegExp; + protocol: RegExp; + replaceNetworkErrorSentry: RegExp; + sanitizeUrl: RegExp; + seedPhrase: RegExp; + startUrl: RegExp; + trailingSlash: RegExp; + trailingZero: RegExp; + transactionNonce: RegExp; + urlHttpToHttps: RegExp; + url: RegExp; + validChainIdHex: RegExp; + validChainId: RegExp; + walletAddress: RegExp; + whiteSpaces: RegExp; +} diff --git a/app/util/sentryUtils.js b/app/util/sentryUtils.js index cf9fb4c07d2..80535d43824 100644 --- a/app/util/sentryUtils.js +++ b/app/util/sentryUtils.js @@ -4,6 +4,7 @@ import { Dedupe, ExtraErrorData } from '@sentry/integrations'; import extractEthJsErrorMessage from './extractEthJsErrorMessage'; import DefaultPreference from 'react-native-default-preference'; import { AGREED, METRICS_OPT_IN } from '../constants/storage'; +import { regex } from './regex'; const METAMASK_ENVIRONMENT = process.env['METAMASK_ENVIRONMENT'] || 'local'; // eslint-disable-line dot-notation @@ -106,10 +107,7 @@ function rewriteReport(report) { function sanitizeUrlsFromErrorMessages(report) { rewriteErrorMessages(report, (errorMessage) => { - const re = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gu; - - const urlsInMessage = errorMessage.match(re); + const urlsInMessage = errorMessage.match(regex.sanitizeUrl); urlsInMessage?.forEach((url) => { if (!ERROR_URL_ALLOWLIST.some((allowedUrl) => url.match(allowedUrl))) { @@ -122,7 +120,10 @@ function sanitizeUrlsFromErrorMessages(report) { function sanitizeAddressesFromErrorMessages(report) { rewriteErrorMessages(report, (errorMessage) => { - const newErrorMessage = errorMessage.replace(/0x[A-Fa-f0-9]{40}/u, '**'); + const newErrorMessage = errorMessage.replace( + regex.replaceNetworkErrorSentry, + '**', + ); return newErrorMessage; }); } diff --git a/app/util/validators/index.js b/app/util/validators/index.js index 37dfd39a4ac..dddc41cc44a 100644 --- a/app/util/validators/index.js +++ b/app/util/validators/index.js @@ -1,5 +1,6 @@ import { ethers } from 'ethers'; import Encryptor from '../../core/Encryptor'; +import { regex } from '../regex'; export const failedSeedPhraseRequirements = (seed) => { const wordCount = seed.split(/\s/u).length; @@ -36,6 +37,7 @@ export const parseVaultValue = async (password, vault) => { }; export const parseSeedPhrase = (seedPhrase) => - (seedPhrase || '').trim().toLowerCase().match(/\w+/gu)?.join(' ') || ''; + (seedPhrase || '').trim().toLowerCase().match(regex.seedPhrase)?.join(' ') || + ''; export const { isValidMnemonic } = ethers.utils;