diff --git a/packages/react-components/src/InputNumber.tsx b/packages/react-components/src/InputNumber.tsx index af39605773..5fdc9dd740 100644 --- a/packages/react-components/src/InputNumber.tsx +++ b/packages/react-components/src/InputNumber.tsx @@ -29,6 +29,7 @@ interface Props { isSi?: boolean; isDecimal?: boolean; isWarning?: boolean; + isSigned?: boolean; isZeroable?: boolean; label?: React.ReactNode; labelExtra?: React.ReactNode; @@ -61,13 +62,13 @@ function getGlobalMaxValue (bitLength?: number): BN { return BN_TWO.pow(new BN(bitLength || DEFAULT_BITLENGTH)).isub(BN_ONE); } -function getRegex (isDecimal: boolean): RegExp { +function getRegex (isDecimal: boolean, isSigned: boolean): RegExp { const decimal = '.'; return new RegExp( isDecimal - ? `^(0|[1-9]\\d*)(\\${decimal}\\d*)?$` - : '^(0|[1-9]\\d*)$' + ? `^${isSigned ? '-?' : ''}(0|[1-9]\\d*)(\\${decimal}\\d*)?$` + : `^${isSigned ? '-?' : ''}(0|[1-9]\\d*)$` ); } @@ -92,10 +93,10 @@ function getSiPowers (si: SiDef | null, decimals?: number): [BN, number, number] return [new BN(basePower + si.power), basePower, si.power]; } -function isValidNumber (bn: BN, bitLength: BitLength, isZeroable: boolean, maxValue?: BN): boolean { +function isValidNumber (bn: BN, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN): boolean { if ( // cannot be negative - bn.lt(BN_ZERO) || + (!isSigned && bn.lt(BN_ZERO)) || // cannot be > than allowed max bn.gt(getGlobalMaxValue(bitLength)) || // check if 0 and it should be a value @@ -111,12 +112,11 @@ function isValidNumber (bn: BN, bitLength: BitLength, isZeroable: boolean, maxVa return true; } -function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN, decimals?: number): [BN, boolean] { +function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN, decimals?: number): [BN, boolean] { const [siPower, basePower, siUnitPower] = getSiPowers(si, decimals); // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec const isDecimalValue = input.match(/^(\d+)\.(\d+)$/); - let result; if (isDecimalValue) { @@ -133,47 +133,46 @@ function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: .add(mod.mul(BN_TEN.pow(new BN(basePower + siUnitPower - modString.length)))); } else { result = new BN(input.replace(/[^\d]/g, '')) - .mul(BN_TEN.pow(siPower)); + .mul(BN_TEN.pow(siPower)) + .muln(isSigned && input.startsWith('-') ? -1 : 1); } return [ result, - isValidNumber(result, bitLength, isZeroable, maxValue) + isValidNumber(result, bitLength, isSigned, isZeroable, maxValue) ]; } -function getValuesFromString (api: ApiPromise, value: string, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN, decimals?: number): [string, BN, boolean] { - const [valueBn, isValid] = inputToBn(api, value, si, bitLength, isZeroable, maxValue, decimals); - +function getValuesFromString (api: ApiPromise, value: string, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN, decimals?: number): [string, BN, boolean] { return [ value, - valueBn, - isValid + ...inputToBn(api, value, si, bitLength, isSigned, isZeroable, maxValue, decimals) ]; } -function getValuesFromBn (valueBn: BN, si: SiDef | null, isZeroable: boolean, _decimals?: number): [string, BN, boolean] { +function getValuesFromBn (valueBn: BN, si: SiDef | null, isSigned: boolean, isZeroable: boolean, _decimals?: number): [string, BN, boolean] { const decimals = isUndefined(_decimals) ? formatBalance.getDefaults().decimals : _decimals; - const value = si - ? valueBn.div(BN_TEN.pow(new BN(decimals + si.power))).toString() - : valueBn.toString(); return [ - value, + si + ? valueBn.div(BN_TEN.pow(new BN(decimals + si.power))).toString() + : valueBn.toString(), valueBn, - isZeroable ? true : valueBn.gt(BN_ZERO) + isZeroable || isSigned + ? true + : valueBn.gt(BN_ZERO) ]; } -function getValues (api: ApiPromise, value: BN | string = BN_ZERO, si: SiDef | null, bitLength: BitLength, isZeroable: boolean, maxValue?: BN, decimals?: number): [string, BN, boolean] { +function getValues (api: ApiPromise, value: BN | string = BN_ZERO, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN, decimals?: number): [string, BN, boolean] { return isBn(value) - ? getValuesFromBn(value, si, isZeroable, decimals) - : getValuesFromString(api, value, si, bitLength, isZeroable, maxValue, decimals); + ? getValuesFromBn(value, si, isSigned, isZeroable, decimals) + : getValuesFromString(api, value, si, bitLength, isSigned, isZeroable, maxValue, decimals); } -function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, className = '', defaultValue, help, isDecimal, isFull, isSi, isDisabled, isError = false, isWarning, isZeroable = true, label, labelExtra, maxLength, maxValue, onChange, onEnter, onEscape, placeholder, siDecimals, siDefault, siSymbol, value: propsValue }: Props): React.ReactElement { +function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, className = '', defaultValue, help, isDecimal, isFull, isSi, isDisabled, isError = false, isWarning, isSigned = false, isZeroable = true, label, labelExtra, maxLength, maxValue, onChange, onEnter, onEscape, placeholder, siDecimals, siDefault, siSymbol, value: propsValue }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const [si, setSi] = useState(() => @@ -182,7 +181,7 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas : null ); const [[value, valueBn, isValid], setValues] = useState<[string, BN, boolean]>(() => - getValues(api, propsValue || defaultValue, si, bitLength, isZeroable, maxValue, siDecimals) + getValues(api, propsValue || defaultValue, si, bitLength, isSigned, isZeroable, maxValue, siDecimals) ); const [isPreKeyDown, setIsPreKeyDown] = useState(false); @@ -197,9 +196,9 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas const _onChangeWithSi = useCallback( (input: string, si: SiDef | null) => setValues( - getValuesFromString(api, input, si, bitLength, isZeroable, maxValue, siDecimals) + getValuesFromString(api, input, si, bitLength, isSigned, isZeroable, maxValue, siDecimals) ), - [api, bitLength, isZeroable, maxValue, siDecimals] + [api, bitLength, isSigned, isZeroable, maxValue, siDecimals] ); const _onChange = useCallback( @@ -223,12 +222,12 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas const { selectionEnd: j, selectionStart: i, value } = event.target as HTMLInputElement; const newValue = `${value.substring(0, i || 0)}${event.key}${value.substring(j || 0)}`; - if (!getRegex(isDecimal || !!si).test(newValue)) { + if (!getRegex(isDecimal || !!si, isSigned).test(newValue)) { event.preventDefault(); } } }, - [isDecimal, isPreKeyDown, si] + [isDecimal, isPreKeyDown, isSigned, si] ); const _onKeyUp = useCallback( @@ -244,11 +243,11 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas (event: React.ClipboardEvent): void => { const { value: newValue } = event.target as HTMLInputElement; - if (!getRegex(isDecimal || !!si).test(newValue)) { + if (!getRegex(isDecimal || !!si, isSigned).test(newValue)) { event.preventDefault(); } }, - [isDecimal, si] + [isDecimal, isSigned, si] ); const _onSelectSiUnit = useCallback( @@ -285,7 +284,11 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas onKeyDown={_onKeyDown} onKeyUp={_onKeyUp} onPaste={_onPaste} - placeholder={placeholder || t('Positive number')} + placeholder={placeholder || ( + isSigned + ? t('Valid number') + : t('Positive number') + )} type='text' value={value} > diff --git a/packages/react-params/src/Param/Amount.tsx b/packages/react-params/src/Param/Amount.tsx index d89fe55fd4..6e2ef29527 100644 --- a/packages/react-params/src/Param/Amount.tsx +++ b/packages/react-params/src/Param/Amount.tsx @@ -21,6 +21,12 @@ function getBitLength (registry: Registry, { type }: TypeDef): number { } function Amount ({ className = '', defaultValue: { value }, isDisabled, isError, label, onChange, onEnter, registry, type, withLabel }: Props): React.ReactElement { + const isSigned = useMemo( + // Allow signed inputs for i{8, 16, 32, 64, 128, ...} types + () => /^i\d*$/.test(type.type), + [type] + ); + const defaultValue = useMemo( () => isDisabled ? value instanceof registry.createClass('AccountIndex') @@ -63,6 +69,7 @@ function Amount ({ className = '', defaultValue: { value }, isDisabled, isError, className='full' defaultValue={defaultValue} isError={isError} + isSigned={isSigned} isZeroable label={label} onChange={_onChange}