InputNumber: reject decimals the unit cannot carry

When an amount had more decimals than the selected unit allows, the
function set result to -1 and then overwrote it, so the conversion went
on with a negative exponent. BN computes 10 ** -n as 10 ** n: with 12
chain decimals, "1.1234567890" in milli became 13345678900 instead of
1123456789. Such input is now invalid.

The decimals were also cut to the chain's decimals, which silently
dropped digits of an asset with more decimals than the chain; the cut is
removed, and with it the api argument it was the only use of.
This commit is contained in:
Satoshi Qazi Muhammed authored and SatoshiQaziMuhammed committed 2026-10-10 02:06:12 +04:00
1 parent 45d479d2ec
commit e2c83baac9
2 files changed
+45 -13

No files matched your search

@@ -0,0 +1,33 @@
// Copyright 2017-2026 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
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]);
});
});
+12 -13
View File
@@ -1,13 +1,11 @@
// Copyright 2017-2026 @pezkuwi/react-components authors & contributors // Copyright 2017-2026 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { SiDef } from '@pezkuwi/util/types'; import type { SiDef } from '@pezkuwi/util/types';
import type { BitLength } from './types.js'; import type { BitLength } from './types.js';
import React, { useCallback, useEffect, useState } from 'react'; 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 { BN, BN_ONE, BN_TEN, BN_TWO, BN_ZERO, formatBalance, isBn, isUndefined } from '@pezkuwi/util';
import { TokenUnit } from './InputConsts/units.js'; import { TokenUnit } from './InputConsts/units.js';
@@ -94,7 +92,7 @@ function isValidNumber (bn: BN, bitLength: BitLength, isSigned: boolean, isZeroa
return true; 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); const [siPower, basePower, siUnitPower] = getSiPowers(si, decimals);
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec // 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; let result;
if (isDecimalValue) { 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) { if (siUnitPower - isDecimalValue[2].length < -basePower) {
result = new BN(-1); return [new BN(-1), false];
} }
const div = new BN(input.replace(/\.\d*$/, '')); 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); const mod = new BN(modString);
result = div 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 [ return [
value, 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) return isBn(value)
? getValuesFromBn(value, si, isSigned, isZeroable, decimals) ? 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<Props> { 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<Props> {
const { t } = useTranslation(); const { t } = useTranslation();
const { api } = useApi();
const [si] = useState<SiDef | null>(() => const [si] = useState<SiDef | null>(() =>
isSi isSi
? siDefault || formatBalance.findSi('-') ? siDefault || formatBalance.findSi('-')
: null : null
); );
const [[value, valueBn, isValid], setValues] = useState<[string, BN, boolean]>(() => 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); const [isPreKeyDown, setIsPreKeyDown] = useState(false);
@@ -173,9 +172,9 @@ function InputNumber ({ autoFocus, bitLength = DEFAULT_BITLENGTH, children, clas
const _onChangeWithSi = useCallback( const _onChangeWithSi = useCallback(
(input: string, si: SiDef | null) => setValues( (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( const _onChange = useCallback(