diff --git a/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx b/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx index 9e0e2292d..2da62ad26 100644 --- a/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx +++ b/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx @@ -10,7 +10,7 @@ import { addDecimalsFormatNumber, formatNumber, removePaginationWrapper, - suitableForSyntaxHighlighter, + validForSyntaxHighlighter, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { RouteTitle } from '../../components/route-title'; @@ -134,7 +134,7 @@ export const NetworkParameterRow = ({ }: { row: { key: string; value: string }; }) => { - const isSyntaxRow = suitableForSyntaxHighlighter(value); + const isSyntaxRow = validForSyntaxHighlighter(value); useDocumentTitle(['Network Parameters']); return ( diff --git a/apps/governance/src/routes/proposals/propose/network-parameter/propose-network-parameter.tsx b/apps/governance/src/routes/proposals/propose/network-parameter/propose-network-parameter.tsx index f79f5b91e..f3da6639f 100644 --- a/apps/governance/src/routes/proposals/propose/network-parameter/propose-network-parameter.tsx +++ b/apps/governance/src/routes/proposals/propose/network-parameter/propose-network-parameter.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useForm } from 'react-hook-form'; -import { suitableForSyntaxHighlighter } from '@vegaprotocol/utils'; +import { validForSyntaxHighlighter } from '@vegaprotocol/utils'; import { useNetworkParams } from '@vegaprotocol/network-parameters'; import { getClosingTimestamp, @@ -46,7 +46,7 @@ const SelectedNetworkParamCurrentValue = ({

{t('CurrentValue')}

- {suitableForSyntaxHighlighter(value) ? ( + {validForSyntaxHighlighter(value) ? ( ) : ( { const publicKeys = parseAllowListText(value); - if ( - publicKeys.every((pk) => isValidVegaPublicKey(pk)) - ) { + if (publicKeys.every((pk) => validVegaPublicKey(pk))) { return true; } return t('Invalid public key found in allow list'); diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx index 7de0e71e3..306e074de 100644 --- a/libs/accounts/src/lib/transfer-form.tsx +++ b/libs/accounts/src/lib/transfer-form.tsx @@ -1,13 +1,15 @@ import sortBy from 'lodash/sortBy'; import { - useMaxSafe, - useRequired, - useVegaPublicKey, addDecimal, toBigNum, removeDecimal, addDecimalsFormatNumber, } from '@vegaprotocol/utils'; +import { + useMaxSafe, + useRequired, + useVegaPublicKey, +} from '@vegaprotocol/react-helpers'; import { useT } from './use-t'; import { TradingFormGroup, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-price-tp-sl.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-price-tp-sl.tsx index c638438bd..7f9f16bff 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-price-tp-sl.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-price-tp-sl.tsx @@ -1,7 +1,8 @@ import { Controller, type Control } from 'react-hook-form'; import type { Market } from '@vegaprotocol/markets'; import type { OrderFormValues } from '../../hooks/use-form-values'; -import { determinePriceStep, useValidateAmount } from '@vegaprotocol/utils'; +import { determinePriceStep } from '@vegaprotocol/utils'; +import { useValidateAmount } from '@vegaprotocol/react-helpers'; import { TradingFormGroup, Tooltip, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-size-iceberg.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-size-iceberg.tsx index 13c4a01ec..ee63fa4c0 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-size-iceberg.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-size-iceberg.tsx @@ -1,7 +1,7 @@ import { Controller, type Control } from 'react-hook-form'; import type { Market } from '@vegaprotocol/markets'; import type { OrderFormValues } from '../../hooks/use-form-values'; -import { useValidateAmount } from '@vegaprotocol/utils'; +import { useValidateAmount } from '@vegaprotocol/react-helpers'; import { TradingFormGroup, TradingInput, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx index 6fee6c6d1..7bc53cd7e 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx @@ -8,8 +8,8 @@ import { formatForInput, formatValue, removeDecimal, - useValidateAmount, } from '@vegaprotocol/utils'; +import { useValidateAmount } from '@vegaprotocol/react-helpers'; import { type Control, type UseFormWatch } from 'react-hook-form'; import { useForm, Controller, useController } from 'react-hook-form'; import * as Schema from '@vegaprotocol/types'; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 64cae4e08..e51a97e9f 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -31,10 +31,10 @@ import { useOpenVolume } from '@vegaprotocol/positions'; import { toBigNum, removeDecimal, - useValidateAmount, formatForInput, formatValue, } from '@vegaprotocol/utils'; +import { useValidateAmount } from '@vegaprotocol/react-helpers'; import { activeOrdersProvider } from '@vegaprotocol/orders'; import { getAsset, diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index c5d85477c..4114ccf18 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -1,16 +1,18 @@ import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets'; import { AssetOption } from '@vegaprotocol/assets'; import { + addDecimal, + isAssetTypeERC20, + formatNumber, +} from '@vegaprotocol/utils'; +import { + useLocalStorage, useEthereumAddress, useRequired, useVegaPublicKey, useMinSafe, useMaxSafe, - addDecimal, - isAssetTypeERC20, - formatNumber, -} from '@vegaprotocol/utils'; -import { useLocalStorage } from '@vegaprotocol/react-helpers'; +} from '@vegaprotocol/react-helpers'; import { TradingFormGroup, TradingInput, diff --git a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx index f377f4c7f..fdc58631e 100644 --- a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx +++ b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx @@ -3,10 +3,10 @@ import { getDateTimeFormat, addDecimal, addDecimalsFormatNumber, - useValidateAmount, determinePriceStep, determineSizeStep, } from '@vegaprotocol/utils'; +import { useValidateAmount } from '@vegaprotocol/react-helpers'; import { Size } from '@vegaprotocol/datagrid'; import * as Schema from '@vegaprotocol/types'; import { diff --git a/libs/react-helpers/src/hooks/index.ts b/libs/react-helpers/src/hooks/index.ts index 9747c7773..86f492a1b 100644 --- a/libs/react-helpers/src/hooks/index.ts +++ b/libs/react-helpers/src/hooks/index.ts @@ -12,5 +12,6 @@ export * from './use-theme-switcher'; export * from './use-storybook-theme-observer'; export * from './use-yesterday'; export * from './use-previous'; +export * from './use-validate'; export { useScript } from './use-script'; export { useUserAgent } from './use-user-agent'; diff --git a/libs/react-helpers/src/hooks/use-validate.ts b/libs/react-helpers/src/hooks/use-validate.ts new file mode 100644 index 000000000..81a5843ee --- /dev/null +++ b/libs/react-helpers/src/hooks/use-validate.ts @@ -0,0 +1,123 @@ +import { useCallback } from 'react'; +import BigNumber from 'bignumber.js'; +import * as utils from '@vegaprotocol/utils'; + +// TODO: add i18n to react helpers +const useT = () => (str: string) => str; + +export const useRequired = () => { + const t = useT(); + return useCallback( + (value: string) => { + if (!utils.validRequired(value)) { + return t('Required'); + } + return true; + }, + [t] + ); +}; + +export const useEthereumAddress = () => { + const t = useT(); + return useCallback( + (value: string) => { + if (!utils.validEthAddress(value)) { + return t('Invalid Ethereum address'); + } + return true; + }, + [t] + ); +}; + +export const useVegaPublicKey = () => { + const t = useT(); + return useCallback( + (value: string) => { + if (!utils.validVegaPublicKey(value)) { + return t('Invalid Vega key'); + } + return true; + }, + [t] + ); +}; + +export const useMinSafe = () => { + const t = useT(); + return useCallback( + (min: BigNumber) => (value: string) => { + if (utils.validMinSafe(value, min)) { + return t('Value is below minimum'); + } + return true; + }, + [t] + ); +}; + +export const useMaxSafe = () => { + const t = useT(); + return useCallback( + (max: BigNumber) => (value: string) => { + if (utils.validMaxSafe(value, max)) { + return t('Value is above maximum'); + } + return true; + }, + [t] + ); +}; + +export const useValidateJson = () => { + const t = useT(); + return useCallback( + (value: string) => { + if (!utils.validJSON(value)) { + return t('Must be valid JSON'); + } + return true; + }, + [t] + ); +}; + +export const useValidateUrl = () => { + const t = useT(); + return useCallback( + (value: string) => { + if (!utils.validUrl(value)) { + return t('Invalid URL'); + } + return true; + }, + [t] + ); +}; + +/** Used in deal ticket price/size amounts */ +export const useValidateAmount = () => { + const t = useT(); + return useCallback( + (step: number | string, field: string) => { + return (value?: string) => { + if (!utils.validStep(step, value)) { + if (new BigNumber(step).isEqualTo(1)) { + return t('{{field}} must be whole numbers for this market', { + field, + step, + }); + } + + return t('{{field}} must be a multiple of {{step}} for this market', { + field, + step, + }); + } + return true; + }; + }, + [t] + ); +}; diff --git a/libs/utils/.eslintrc.json b/libs/utils/.eslintrc.json index a871f3e05..69409f8d1 100644 --- a/libs/utils/.eslintrc.json +++ b/libs/utils/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*", "__generated__"], + "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], @@ -9,16 +9,15 @@ "error", { "paths": [ - "error", "@apollo/client", + "@ethersproject", "@vegaprotocol/data-provider", "ag-grid-react", "ag-grid-community", + "ethers", "graphql", "graphql-tag", - "graphql-ws", - "ethers", - "@ethersproject" + "graphql-ws" ], "patterns": ["@sentry/*"] } diff --git a/libs/utils/src/lib/validate/common.spec.ts b/libs/utils/src/lib/validate/common.spec.ts index bf10fc66f..f935cf5e0 100644 --- a/libs/utils/src/lib/validate/common.spec.ts +++ b/libs/utils/src/lib/validate/common.spec.ts @@ -1,40 +1,66 @@ -import { renderHook } from '@testing-library/react'; -import { useEthereumAddress, useVegaPublicKey } from './common'; - -it('ethereumAddress', () => { - const result = renderHook(useEthereumAddress); - const ethereumAddress = result.result.current; - - const errorMessage = 'Invalid Ethereum address'; +import { validEthAddress, validVegaPublicKey, validStep } from './common'; +it('validEthAddress', () => { const validAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853'; - expect(ethereumAddress(validAddress)).toEqual(true); + expect(validEthAddress(validAddress)).toEqual(true); const invalidChars = '0xzzc22822A19D20DE7e426fB84aa047399Ddd8853'; - expect(ethereumAddress(invalidChars)).toEqual(errorMessage); + expect(validEthAddress(invalidChars)).toEqual(false); const tooManyChars = '0x72c22822A19D20DE7e426fB84aa047399Ddd88531111111'; - expect(ethereumAddress(tooManyChars)).toEqual(errorMessage); + expect(validEthAddress(tooManyChars)).toEqual(false); const no0x = '1x72c22822A19D20DE7e426fB84aa047399Ddd8853'; - expect(ethereumAddress(no0x)).toEqual(errorMessage); + expect(validEthAddress(no0x)).toEqual(false); }); -it('vegaPublicKey', () => { - const result = renderHook(useVegaPublicKey); - const vegaPublicKey = result.result.current; - - const errorMessage = 'Invalid Vega key'; - +it('validVegaPublicKey', () => { const validKey = '70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680'; - expect(vegaPublicKey(validKey)).toEqual(true); + expect(validVegaPublicKey(validKey)).toEqual(true); const invalidChars = 'zzz14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680'; - expect(vegaPublicKey(invalidChars)).toEqual(errorMessage); + expect(validVegaPublicKey(invalidChars)).toEqual(false); const tooManyChars = '70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680111111'; - expect(vegaPublicKey(tooManyChars)).toEqual(errorMessage); + expect(validVegaPublicKey(tooManyChars)).toEqual(false); +}); + +describe('validateAgainstStep', () => { + it('fails when step is an empty string', () => { + expect(validStep('', '1234')).toEqual(false); + }); + + it.each([ + [0, 0], + [1234567890, 0], + [0.03, 0.03], + [0.09, 0.03], + [0.27, 0.03], + [1, 1], + [123, 1], + [4, 2], + [8, 2], + ])( + 'checks whether given value (%s) IS a multiple of given step (%s)', + (value, step) => { + expect(validStep(step, value)).toEqual(true); + } + ); + + it.each([ + [1, 2], + [0.1, 0.003], + [1.11, 0.1], + [123.1, 1], + [222, 221], + [NaN, 1], + ])( + 'checks whether given value (%s) IS NOT a multiple of given step (%s)', + (value, step) => { + expect(validStep(step, value)).toEqual(false); + } + ); }); diff --git a/libs/utils/src/lib/validate/common.ts b/libs/utils/src/lib/validate/common.ts index c0f0b33bb..169dc9ec6 100644 --- a/libs/utils/src/lib/validate/common.ts +++ b/libs/utils/src/lib/validate/common.ts @@ -1,77 +1,52 @@ import BigNumber from 'bignumber.js'; -import { useT } from '../use-t'; -import { useCallback } from 'react'; -export const useRequired = () => { - const t = useT(); - return useCallback( - (value: string) => { - if (value === null || value === undefined || value === '') { - return t('Required'); - } - return true; - }, - [t] - ); -}; - -export const useEthereumAddress = () => { - const t = useT(); - return useCallback( - (value: string) => { - if (!/^0x[0-9a-fA-F]{40}$/i.test(value)) { - return t('Invalid Ethereum address'); - } - return true; - }, - [t] - ); +export const validRequired = (value: string | number | undefined | null) => { + if (value === null || value === undefined || value === '') { + return false; + } + return true; }; export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i; -export const isValidVegaPublicKey = (value: string) => { +export const validVegaPublicKey = (value: string) => { return VEGA_ID_REGEX.test(value); }; -export const useVegaPublicKey = () => { - const t = useT(); - return useCallback( - (value: string) => { - if (!isValidVegaPublicKey(value)) { - return t('Invalid Vega key'); - } - return true; - }, - [t] - ); + +export const URL_REGEX = + /^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/; +export const validUrl = (value: string) => { + return URL_REGEX.test(value); }; -export const useMinSafe = () => { - const t = useT(); - return useCallback( - (min: BigNumber) => (value: string) => { - if (new BigNumber(value).isLessThan(min)) { - return t('Value is below minimum'); - } - return true; - }, - [t] - ); +export const ETH_ADDRESS = /^0x[0-9a-fA-F]{40}$/i; +export const validEthAddress = (value: string) => { + return ETH_ADDRESS.test(value); }; -export const useMaxSafe = () => { - const t = useT(); - return useCallback( - (max: BigNumber) => (value: string) => { - if (new BigNumber(value).isGreaterThan(max)) { - return t('Value is above maximum'); - } - return true; - }, - [t] - ); +export const validMinSafe = ( + value: string | number | BigNumber, + min: string | number | BigNumber +) => { + return new BigNumber(value).isLessThan(min); }; -export const suitableForSyntaxHighlighter = (str: string) => { +export const validMaxSafe = ( + value: string | number | BigNumber, + max: string | number | BigNumber +) => { + return new BigNumber(value).isGreaterThan(max); +}; + +export const validJSON = (value: string) => { + try { + JSON.parse(value); + return true; + } catch (e) { + return false; + } +}; + +export const validForSyntaxHighlighter = (str: string) => { try { const test = JSON.parse(str); return test && Object.keys(test).length > 0; @@ -80,35 +55,17 @@ export const suitableForSyntaxHighlighter = (str: string) => { } }; -export const useValidateJson = () => { - const t = useT(); - return useCallback( - (value: string) => { - try { - JSON.parse(value); - return true; - } catch (e) { - return t('Must be valid JSON'); - } - }, - [t] - ); -}; +export const validStep = (step: string | number, input?: string | number) => { + const stepValue = new BigNumber(step); + if (stepValue.isNaN()) { + // unable to check if step is not a number + return false; + } + if (stepValue.isZero()) { + // every number is valid when step is zero + return true; + } -export const URL_REGEX = - /^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/; -const isValidUrl = (value: string) => { - return URL_REGEX.test(value); -}; -export const useValidateUrl = () => { - const t = useT(); - return useCallback( - (value: string) => { - if (!isValidUrl(value)) { - return t('Invalid URL'); - } - return true; - }, - [t] - ); + const value = new BigNumber(input || ''); + return value.modulo(stepValue).isZero(); }; diff --git a/libs/utils/src/lib/validate/index.ts b/libs/utils/src/lib/validate/index.ts index 2b21ad686..d0b932366 100644 --- a/libs/utils/src/lib/validate/index.ts +++ b/libs/utils/src/lib/validate/index.ts @@ -1,2 +1 @@ export * from './common'; -export * from './validate-amount'; diff --git a/libs/utils/src/lib/validate/validate-amount.spec.ts b/libs/utils/src/lib/validate/validate-amount.spec.ts deleted file mode 100644 index ea019b440..000000000 --- a/libs/utils/src/lib/validate/validate-amount.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { validateAgainstStep } from './validate-amount'; - -describe('validateAgainstStep', () => { - it('fails when step is an empty string', () => { - expect(validateAgainstStep('', '1234')).toEqual(false); - }); - - it.each([ - [0, 0], - [1234567890, 0], - [0.03, 0.03], - [0.09, 0.03], - [0.27, 0.03], - [1, 1], - [123, 1], - [4, 2], - [8, 2], - ])( - 'checks whether given value (%s) IS a multiple of given step (%s)', - (value, step) => { - expect(validateAgainstStep(step, value)).toEqual(true); - } - ); - - it.each([ - [1, 2], - [0.1, 0.003], - [1.11, 0.1], - [123.1, 1], - [222, 221], - [NaN, 1], - ])( - 'checks whether given value (%s) IS NOT a multiple of given step (%s)', - (value, step) => { - expect(validateAgainstStep(step, value)).toEqual(false); - } - ); -}); diff --git a/libs/utils/src/lib/validate/validate-amount.ts b/libs/utils/src/lib/validate/validate-amount.ts deleted file mode 100644 index e5ddde5a9..000000000 --- a/libs/utils/src/lib/validate/validate-amount.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback } from 'react'; -import { useT } from '../use-t'; -import BigNumber from 'bignumber.js'; - -export const useValidateAmount = () => { - const t = useT(); - return useCallback( - (step: number | string, field: string) => { - return (value?: string) => { - const isValid = validateAgainstStep(step, value); - if (!isValid) { - if (new BigNumber(step).isEqualTo(1)) { - return t('{{field}} must be whole numbers for this market', { - field, - step, - }); - } - - return t('{{field}} must be a multiple of {{step}} for this market', { - field, - step, - }); - } - return true; - }; - }, - [t] - ); -}; - -const isMultipleOf = (value: BigNumber, multipleOf: BigNumber) => - value.modulo(multipleOf).isZero(); - -export const validateAgainstStep = ( - step: string | number, - input?: string | number -) => { - const stepValue = new BigNumber(step); - if (stepValue.isNaN()) { - // unable to check if step is not a number - return false; - } - if (stepValue.isZero()) { - // every number is valid when step is zero - return true; - } - - const value = new BigNumber(input || ''); - return isMultipleOf(value, stepValue); -}; diff --git a/libs/wallet/src/connectors/view-party-connector.ts b/libs/wallet/src/connectors/view-party-connector.ts index 0a708691f..01234e150 100644 --- a/libs/wallet/src/connectors/view-party-connector.ts +++ b/libs/wallet/src/connectors/view-party-connector.ts @@ -1,6 +1,6 @@ import { type StoreApi } from 'zustand'; import { type Store, type Connector } from '../types'; -import { isValidVegaPublicKey } from '@vegaprotocol/utils'; +import { validVegaPublicKey } from '@vegaprotocol/utils'; import { ConnectorError, chainIdError, @@ -40,7 +40,7 @@ export class ViewPartyConnector implements Connector { throw userRejectedError(); } - if (!isValidVegaPublicKey(value)) { + if (!validVegaPublicKey(value)) { throw connectError('invalid public key'); } diff --git a/libs/withdraws/src/lib/withdraw-form.tsx b/libs/withdraws/src/lib/withdraw-form.tsx index 5f23a3cae..611228f18 100644 --- a/libs/withdraws/src/lib/withdraw-form.tsx +++ b/libs/withdraws/src/lib/withdraw-form.tsx @@ -1,13 +1,15 @@ import type { Asset } from '@vegaprotocol/assets'; import { AssetOption } from '@vegaprotocol/assets'; import { - useEthereumAddress, - useRequired, - useMinSafe, removeDecimal, isAssetTypeERC20, formatNumber, } from '@vegaprotocol/utils'; +import { + useEthereumAddress, + useRequired, + useMinSafe, +} from '@vegaprotocol/react-helpers'; import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { TradingFormGroup,