diff --git a/packages/react-components/src/InputNumber.spec.tsx b/packages/react-components/src/InputNumber.spec.tsx new file mode 100644 index 0000000..2e1a10d --- /dev/null +++ b/packages/react-components/src/InputNumber.spec.tsx @@ -0,0 +1,33 @@ +// Copyright 2017-2026 @pezkuwi/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { formatBalance } from '@pezkuwi/util'; + +import { inputToBn } from './InputNumber.js'; + +function convert (input: string, unit: string, decimals: number): [string, boolean] { + const [bn, isValid] = inputToBn(input, formatBalance.findSi(unit), 128, false, true, undefined, decimals); + + return [bn.toString(), isValid]; +} + +describe('inputToBn', (): void => { + it('converts a decimal in the base unit', (): void => { + expect(convert('1.5', '-', 12)).toEqual(['1500000000000', true]); + }); + + it('converts every decimal a smaller unit can carry', (): void => { + expect(convert('1.123456789', 'm', 12)).toEqual(['1123456789', true]); + }); + + it('rejects more decimals than the unit can carry instead of scaling them up', (): void => { + // the exponent would be 9 - 10 = -1, and BN computes 10 ** -1 as 10 + expect(convert('1.1234567890', 'm', 12)).toEqual(['-1', false]); + }); + + it('keeps decimals beyond the chain decimals for an asset with more', (): void => { + expect(convert('1.0000000000001', '-', 18)).toEqual(['1000000000000100000', true]); + }); +}); diff --git a/packages/react-components/src/InputNumber.tsx b/packages/react-components/src/InputNumber.tsx index e7d9d32..7f6b6c8 100644 --- a/packages/react-components/src/InputNumber.tsx +++ b/packages/react-components/src/InputNumber.tsx @@ -1,13 +1,11 @@ // Copyright 2017-2026 @pezkuwi/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ApiPromise } from '@pezkuwi/api'; import type { SiDef } from '@pezkuwi/util/types'; import type { BitLength } from './types.js'; import React, { useCallback, useEffect, useState } from 'react'; -import { useApi } from '@pezkuwi/react-hooks'; import { BN, BN_ONE, BN_TEN, BN_TWO, BN_ZERO, formatBalance, isBn, isUndefined } from '@pezkuwi/util'; import { TokenUnit } from './InputConsts/units.js'; @@ -94,7 +92,7 @@ function isValidNumber (bn: BN, bitLength: BitLength, isSigned: boolean, isZeroa return true; } -function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN | null, decimals?: number): [BN, boolean] { +export function inputToBn (input: string, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN | null, decimals?: number): [BN, boolean] { const [siPower, basePower, siUnitPower] = getSiPowers(si, decimals); // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec @@ -102,12 +100,14 @@ function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: let result; if (isDecimalValue) { + // More decimals than the unit can carry: the exponent below would be + // negative, and BN treats 10 ** -n as 10 ** n. if (siUnitPower - isDecimalValue[2].length < -basePower) { - result = new BN(-1); + return [new BN(-1), false]; } const div = new BN(input.replace(/\.\d*$/, '')); - const modString = input.replace(/^\d+\./, '').substring(0, api.registry.chainDecimals[0]); + const modString = input.replace(/^\d+\./, ''); const mod = new BN(modString); result = div @@ -125,10 +125,10 @@ function inputToBn (api: ApiPromise, input: string, si: SiDef | null, bitLength: ]; } -function getValuesFromString (api: ApiPromise, value: string, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN | null, decimals?: number): [string, BN, boolean] { +function getValuesFromString (value: string, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN | null, decimals?: number): [string, BN, boolean] { return [ value, - ...inputToBn(api, value, si, bitLength, isSigned, isZeroable, maxValue, decimals) + ...inputToBn(value, si, bitLength, isSigned, isZeroable, maxValue, decimals) ]; } @@ -148,22 +148,21 @@ function getValuesFromBn (valueBn: BN, si: SiDef | null, isSigned: boolean, isZe ]; } -function getValues (api: ApiPromise, value: BN | string = BN_ZERO, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN | null, decimals?: number): [string, BN, boolean] { +function getValues (value: BN | string = BN_ZERO, si: SiDef | null, bitLength: BitLength, isSigned: boolean, isZeroable: boolean, maxValue?: BN | null, decimals?: number): [string, BN, boolean] { return isBn(value) ? getValuesFromBn(value, si, isSigned, isZeroable, decimals) - : getValuesFromString(api, value, si, bitLength, isSigned, isZeroable, maxValue, decimals); + : getValuesFromString(value, si, bitLength, isSigned, isZeroable, maxValue, decimals); } function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, className = '', defaultValue, isDecimal, isDisabled, isError = false, isFull, isLoading, isSi, isSigned = false, isWarning, 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] = useState(() => isSi ? siDefault || formatBalance.findSi('-') : null ); const [[value, valueBn, isValid], setValues] = useState<[string, BN, boolean]>(() => - getValues(api, propsValue || defaultValue, si, bitLength, isSigned, isZeroable, maxValue, siDecimals) + getValues(propsValue || defaultValue, si, bitLength, isSigned, isZeroable, maxValue, siDecimals) ); const [isPreKeyDown, setIsPreKeyDown] = useState(false); @@ -173,9 +172,9 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas const _onChangeWithSi = useCallback( (input: string, si: SiDef | null) => setValues( - getValuesFromString(api, input, si, bitLength, isSigned, isZeroable, maxValue, siDecimals) + getValuesFromString(input, si, bitLength, isSigned, isZeroable, maxValue, siDecimals) ), - [api, bitLength, isSigned, isZeroable, maxValue, siDecimals] + [bitLength, isSigned, isZeroable, maxValue, siDecimals] ); const _onChange = useCallback(