diff --git a/src/components/InputNumber.js b/src/components/InputNumber.js index 0367f168..871d7501 100644 --- a/src/components/InputNumber.js +++ b/src/components/InputNumber.js @@ -8,7 +8,7 @@ import React, { useState, useLayoutEffect, useRef, useEffect } from "react"; import PropTypes from "prop-types"; import { useSelector } from 'react-redux'; -import hathorLib from "@hathor/wallet-lib"; +import { numberUtils } from "@hathor/wallet-lib"; /** * Component that enhances typing numbers @@ -28,43 +28,36 @@ const InputNumber = React.forwardRef( ( { defaultValue, - precision, - separator, - locale, + isNFT, onValueChange, requirePositive, ...otherProps }, ref ) => { - - const decimalPlaces = useSelector((state) => state.serverInfo.decimalPlaces); - const decimalPrecision = precision ?? decimalPlaces; + if (ref !== null) { + // TODO: We should just convert InputNumber from a React.forwardRef to a normal React.Component, + // but we may do this in a separate PR + throw Error('do not use ref in InputNumber, instead use onValueChange') + } + const decimalPlaces = isNFT ? 0 : useSelector((state) => state.serverInfo.decimalPlaces); /** - * Formats a string following the pattern 9,999.99. It decomposes rawValue into decimal and fractional parts, mainly to add the thousands separator. + * Formats a number following the pattern 9,999.99 * - * @param {string} rawValue String to be formatted + * @param {number} rawValue Number to be formatted * * @return {string} Formatted string */ - const format = (rawValue = "") => { - const value = String(rawValue) - .replace(/[^\d]/g, "") - .padStart(decimalPrecision + 1, "0"); - const decimalPart = Intl.NumberFormat(locale).format( - value.substr(0, value.length - decimalPrecision) - ); - const fractionalPart = value.substr(value.length - decimalPrecision); - if (fractionalPart.length === 0) { - return decimalPart; - } else { - return `${decimalPart}${separator}${fractionalPart}`; + const format = (rawValue) => { + if (typeof rawValue !== 'number') { + throw Error(`value must be a number: ${rawValue}`) } + return numberUtils.prettyValue(rawValue, decimalPlaces) }; - const innerRef = ref || useRef(); - const [value, setValue] = useState(format(defaultValue)); + const innerRef = useRef(); + const [value, setValue] = useState(defaultValue); /** * Listen keydown events while this component is focused overriding the default native input behavior. @@ -78,7 +71,7 @@ const InputNumber = React.forwardRef( const isBackspace = evt.key === "Backspace" || evt.key === "Delete"; const isDeleteAll = isBackspace && - evt.target.selectionEnd - evt.target.selectionStart >= value.length; + evt.target.selectionEnd - evt.target.selectionStart >= format(value).length; const isCtrlOrMeta = evt.ctrlKey || evt.metaKey; // Do not handle keyboard events when ctrlKey or metaKey are present @@ -88,36 +81,34 @@ const InputNumber = React.forwardRef( let newValue = value; if (isDeleteAll) { - newValue = ""; + newValue = 0; } else if (isNumberChar) { - newValue = value.concat(evt.key); + newValue = value * 10 + Number(evt.key); } else if (isBackspace) { - newValue = value.slice(0, -1); + newValue = Math.floor(value / 10); } - newValue = format(newValue); updateCaretPosition(newValue); return newValue; }); /** * Handle onClick events just to update the caret position. - * - * @param {MouseEvent} evt MouseEvent triggered when the input or its inner content is clicked */ - const onClick = (evt) => { - updateCaretPosition(format(evt.target.value)); - }; + const onClick = () => setValue((currentValue) => { + updateCaretPosition(currentValue); + return currentValue; + }); /** * Put the caret always at the end. * - * @param {string} value Current input value + * @param {number} value Current input value */ const updateCaretPosition = (value) => { setTimeout(() => { const { current } = innerRef; if (current) { - current.selectionStart = value.length; + current.selectionStart = format(value).length; } }); }; @@ -131,11 +122,10 @@ const InputNumber = React.forwardRef( */ const onPaste = (evt) => setValue(() => { - const paste = format( - (evt.clipboardData || window.clipboardData).getData("text") - ); - updateCaretPosition(paste); - return paste; + const paste = (evt.clipboardData || window.clipboardData).getData("text"); + const newValue = Number(paste.replace(/\D/g, '')) + updateCaretPosition(newValue); + return newValue; }); /** @@ -159,17 +149,15 @@ const InputNumber = React.forwardRef( * Call onValueChange every time the value changes, similarly the native onChange callback. */ useEffect(() => { - const parsedValue = - Number(value.replace(/[^\d]/g, "")) / Math.pow(10, decimalPrecision); - onValueChange(parsedValue); - if (requirePositive && parsedValue <= 0) { + onValueChange(value); + if (requirePositive && value <= 0) { innerRef.current.setCustomValidity('Must be a positive number.'); } else { innerRef.current.setCustomValidity(''); } }, [value]); - return ; + return ; } ); @@ -177,27 +165,15 @@ InputNumber.propTypes = { /** * Same behavior of React input defaultValue */ - defaultValue: PropTypes.string, + defaultValue: PropTypes.number, /** - * Number of digits after the separator + * Whether this is a NFT input */ - precision: PropTypes.number, - /** - * Generally a dot or a comma char - */ - separator: PropTypes.string, - /** - * Locale (e.g.: 'en-US', 'pt-br'). Must be used in conjunction with `separator` - */ - locale: PropTypes.string, + isNFT: PropTypes.bool, /** * Similar to onChange, but it receives the parsed value as single parameter */ onValueChange: PropTypes.func, - /** - * Same behavior of React input onChange - */ - onChange: PropTypes.func, /** * If the input value is required to be a positive number, i.e. > 0 */ @@ -205,12 +181,9 @@ InputNumber.propTypes = { }; InputNumber.defaultProps = { - defaultValue: "", - precision: null, - separator: ".", - locale: "en-US", + defaultValue: 0, + isNFT: false, onValueChange: () => {}, - onChange: () => {}, requirePositive: false, }; diff --git a/src/components/OutputsWrapper.js b/src/components/OutputsWrapper.js index 67c8b6b1..c2ccbcf9 100644 --- a/src/components/OutputsWrapper.js +++ b/src/components/OutputsWrapper.js @@ -9,7 +9,6 @@ import React from 'react'; import { t } from 'ttag'; import $ from 'jquery'; import _ from 'lodash'; -import hathorLib from '@hathor/wallet-lib'; import InputNumber from './InputNumber'; import LOCAL_STORE from '../storage'; import { connect } from 'react-redux'; @@ -30,7 +29,7 @@ class OutputsWrapper extends React.Component { super(props); this.address = React.createRef(); - this.value = React.createRef(); + this.value = null; this.timelock = React.createRef(); this.timelockCheckbox = React.createRef(); this.uniqueID = _.uniqueId() @@ -55,11 +54,7 @@ class OutputsWrapper extends React.Component { render = () => { const renderInputNumber = () => { const classNames = "form-control output-value col-2"; - if (this.props.isNFT) { - return ; - } else { - return ; - } + return this.value = value} className={classNames} isNFT={this.props.isNFT} />; } return ( diff --git a/src/components/SendTokensOne.js b/src/components/SendTokensOne.js index 87c53653..3693ba57 100644 --- a/src/components/SendTokensOne.js +++ b/src/components/SendTokensOne.js @@ -106,16 +106,15 @@ class SendTokensOne extends React.Component { let data = {'outputs': [], 'inputs': []}; for (const output of this.outputs) { const address = output.current.address.current.value.replace(/\s/g, ''); - const valueStr = (output.current.value.current.value || "").replace(/,/g, ''); + const value = output.current.value; - if (address && valueStr) { + if (address && value) { // Doing the check here because need to validate before doing parseInt - const tokensValue = this.isNFT() ? parseInt(valueStr) : wallet.decimalToInteger(valueStr, this.props.decimalPlaces); - if (tokensValue > hathorLib.constants.MAX_OUTPUT_VALUE) { + if (value > hathorLib.constants.MAX_OUTPUT_VALUE) { this.props.updateState({ errorMessage: `Token: ${this.state.selected.symbol}. Output: ${output.current.props.index}. Maximum output value is ${hathorLib.numberUtils.prettyValue(hathorLib.constants.MAX_OUTPUT_VALUE, this.isNFT() ? 0 : this.props.decimalPlaces)}` }); return null; } - let dataOutput = {'address': address, 'value': parseInt(tokensValue, 10), 'token': this.state.selected.uid}; + let dataOutput = {'address': address, 'value': value, 'token': this.state.selected.uid}; const hasTimelock = output.current.timelockCheckbox.current.checked; if (hasTimelock) { diff --git a/src/components/atomic-swap/ModalAtomicReceive.js b/src/components/atomic-swap/ModalAtomicReceive.js index 4388b383..5e45ed4e 100644 --- a/src/components/atomic-swap/ModalAtomicReceive.js +++ b/src/components/atomic-swap/ModalAtomicReceive.js @@ -8,7 +8,7 @@ import React, { useState, useRef, useEffect } from "react"; import { t } from "ttag"; import InputNumber from "../InputNumber"; -import hathorLib, { Address } from "@hathor/wallet-lib"; +import { Address } from "@hathor/wallet-lib"; import walletUtils from '../../utils/wallet'; import { getGlobalWallet } from "../../modules/wallet"; import { useSelector } from 'react-redux'; @@ -18,7 +18,6 @@ export function ModalAtomicReceive ({ sendClickHandler, receivableTokens, manage const wallet = getGlobalWallet(); const [selectedToken, setSelectedToken] = useState(receivableTokens[0]); const [address, setAddress] = useState(''); - let amountRef = useRef(); const [amount, setAmount] = useState(0); const [errMessage, setErrMessage] = useState(''); const modalDomId = 'atomicReceiveModal'; @@ -69,7 +68,7 @@ export function ModalAtomicReceive ({ sendClickHandler, receivableTokens, manage // On success, clean error message and return user input setErrMessage(''); - sendClickHandler({ selectedToken, address, amount: walletUtils.decimalToInteger(amount, decimalPlaces) }); + sendClickHandler({ selectedToken, address, amount }); onClose(`#${modalDomId}`); } @@ -116,9 +115,7 @@ export function ModalAtomicReceive ({ sendClickHandler, receivableTokens, manage setAmount(value)} className="form-control output-value col-3"/> diff --git a/src/components/atomic-swap/ModalAtomicSend.js b/src/components/atomic-swap/ModalAtomicSend.js index 290cddb1..24911d03 100644 --- a/src/components/atomic-swap/ModalAtomicSend.js +++ b/src/components/atomic-swap/ModalAtomicSend.js @@ -8,7 +8,7 @@ import React, { useEffect, useRef, useState } from "react"; import { t } from "ttag"; import InputNumber from "../InputNumber"; -import hathorLib, { Address, numberUtils } from "@hathor/wallet-lib"; +import { Address, numberUtils } from "@hathor/wallet-lib"; import { translateTxToProposalUtxo } from "../../utils/atomicSwap"; import { TOKEN_DOWNLOAD_STATUS } from "../../sagas/tokens"; import { get } from 'lodash'; @@ -130,7 +130,6 @@ function UtxoSelection ({ wallet, utxos, token, utxosChanged, setErrMessage }) { export function ModalAtomicSend ({ sendClickHandler, sendableTokens, tokenBalances, manageDomLifecycle, onClose, wallet }) { const [selectedToken, setSelectedToken] = useState(sendableTokens.length && sendableTokens[0]); const [changeAddress, setChangeAddress] = useState(''); - let amountRef = useRef(); const [amount, setAmount] = useState(0); const [errMessage, setErrMessage] = useState(''); const modalDomId = 'atomicSendModal'; @@ -203,9 +202,8 @@ export function ModalAtomicSend ({ sendClickHandler, sendableTokens, tokenBalanc } // Validating available balance - const selectedAmount = walletUtils.decimalToInteger(amount, decimalPlaces); const availableAmount = selectedTokenBalance.data.available; - if (selectedAmount > availableAmount) { + if (amount > availableAmount) { setErrMessage(t`Insufficient balance`); return false; } @@ -214,7 +212,7 @@ export function ModalAtomicSend ({ sendClickHandler, sendableTokens, tokenBalanc if (showUtxoSelection) { const validUtxos = utxos.filter(u => u.amount); const totalAmount = validUtxos.reduce((acc, u) => acc + u.amount, 0); - if (selectedAmount > totalAmount) { + if (amount > totalAmount) { setErrMessage(t`Insufficient balance on selected inputs`); return false; } @@ -239,7 +237,7 @@ export function ModalAtomicSend ({ sendClickHandler, sendableTokens, tokenBalanc sendClickHandler({ selectedToken, changeAddress: changeAddress, - amount: walletUtils.decimalToInteger(amount, decimalPlaces), + amount, utxos: selectedUtxos, }); onClose(`#${modalDomId}`); @@ -325,9 +323,7 @@ export function ModalAtomicSend ({ sendClickHandler, sendableTokens, tokenBalanc setAmount(value)} className="form-control output-value col-3"/> diff --git a/src/components/tokens/TokenMelt.js b/src/components/tokens/TokenMelt.js index d3742a81..ef22ffb7 100644 --- a/src/components/tokens/TokenMelt.js +++ b/src/components/tokens/TokenMelt.js @@ -42,8 +42,6 @@ class TokenMelt extends React.Component { constructor(props) { super(props); - // Reference to amount input - this.amount = React.createRef(); // Reference to create another melt output checkbox this.createAnother = React.createRef(); } @@ -58,11 +56,10 @@ class TokenMelt extends React.Component { * In case of error, an object with {success: false, message} */ prepareSendTransaction = async (pin) => { - const amountValue = this.isNFT() ? this.state.amount : walletUtils.decimalToInteger(this.state.amount, this.props.decimalPlaces); const wallet = getGlobalWallet(); const transaction = await wallet.prepareMeltTokensData( this.props.token.uid, - amountValue, + this.state.amount, { createAnotherMelt: this.createAnother.current.checked, pinCode: pin @@ -90,8 +87,7 @@ class TokenMelt extends React.Component { * Return a message to be shown in case of success */ getSuccessMessage = () => { - const amount = this.isNFT() ? this.state.amount : walletUtils.decimalToInteger(this.state.amount, this.props.decimalPlaces); - const prettyAmountValue = hathorLib.numberUtils.prettyValue(amount, this.isNFT() ? 0 : this.props.decimalPlaces); + const prettyAmountValue = hathorLib.numberUtils.prettyValue(this.state.amount, this.isNFT() ? 0 : this.props.decimalPlaces); return t`${prettyAmountValue} ${this.props.token.symbol} melted!`; } @@ -102,10 +98,9 @@ class TokenMelt extends React.Component { * @return {string} Error message, in case of form invalid. Nothing, otherwise. */ melt = () => { - const amountValue = this.isNFT() ? this.state.amount : walletUtils.decimalToInteger(this.state.amount, this.props.decimalPlaces); const walletAmount = get(this.props.tokenBalance, 'data.available', 0); - if (amountValue > walletAmount) { + if (this.state.amount > walletAmount) { const prettyWalletAmount = hathorLib.numberUtils.prettyValue(walletAmount, this.isNFT() ? 0 : this.props.decimalPlaces); return t`The total amount you have is only ${prettyWalletAmount}.`; } @@ -119,30 +114,14 @@ class TokenMelt extends React.Component { } render() { - const renderInputNumber = () => { - if (this.isNFT()) { - return ( - - ) - } else { - return ( - - ) - } - } + const renderInputNumber = () => ( + + ) const renderForm = () => { return ( diff --git a/src/components/tokens/TokenMint.js b/src/components/tokens/TokenMint.js index f0de64ef..cb5deb9d 100644 --- a/src/components/tokens/TokenMint.js +++ b/src/components/tokens/TokenMint.js @@ -61,12 +61,11 @@ class TokenMint extends React.Component { * In case of error, an object with {success: false, message} */ prepareSendTransaction = async (pin) => { - const amountValue = this.isNFT() ? this.state.amount : walletUtils.decimalToInteger(this.state.amount, this.props.decimalPlaces); const address = this.chooseAddress.current.checked ? null : this.address.current.value; const wallet = getGlobalWallet(); const transaction = await wallet.prepareMintTokensData( this.props.token.uid, - amountValue, + this.state.amount, { address, createAnotherMint: this.createAnother.current.checked, @@ -95,8 +94,7 @@ class TokenMint extends React.Component { * Return a message to be shown in case of success */ getSuccessMessage = () => { - const amount = this.isNFT() ? this.state.amount : walletUtils.decimalToInteger(this.state.amount, this.props.decimalPlaces); - const prettyAmountValue = hathorLib.numberUtils.prettyValue(amount, this.isNFT() ? 0 : this.props.decimalPlaces); + const prettyAmountValue = hathorLib.numberUtils.prettyValue(this.state.amount, this.isNFT() ? 0 : this.props.decimalPlaces); return t`${prettyAmountValue} ${this.props.token.symbol} minted!`; } @@ -152,28 +150,14 @@ class TokenMint extends React.Component { ); } - const renderInputNumber = () => { - if (this.isNFT()) { - return ( - - ); - } else { - return ( - - ); - } - } + const renderInputNumber = () => ( + + ); const renderForm = () => { return ( diff --git a/src/screens/CreateNFT.js b/src/screens/CreateNFT.js index 54ce05a9..1856b452 100644 --- a/src/screens/CreateNFT.js +++ b/src/screens/CreateNFT.js @@ -314,7 +314,7 @@ function CreateNFT() {
- +
diff --git a/src/screens/CreateToken.js b/src/screens/CreateToken.js index 6589c1b5..4f92db68 100644 --- a/src/screens/CreateToken.js +++ b/src/screens/CreateToken.js @@ -68,8 +68,7 @@ function CreateToken() { } // Validating maximum amount - const tokensValue = walletUtils.decimalToInteger(amount, decimalPlaces); - if (tokensValue > hathorLib.constants.MAX_OUTPUT_VALUE) { + if (amount > hathorLib.constants.MAX_OUTPUT_VALUE) { const max_output_value_str = hathorLib.numberUtils.prettyValue(hathorLib.constants.MAX_OUTPUT_VALUE, decimalPlaces); setErrorMessage(t`Maximum value to mint token is ${max_output_value_str}`); return false; @@ -122,7 +121,7 @@ function CreateToken() { transaction = await wallet.prepareCreateNewToken( shortNameRef.current.value, symbolRef.current.value, - walletUtils.decimalToInteger(amount, decimalPlaces), + amount, { address, pinCode: pin } ); @@ -276,7 +275,6 @@ function CreateToken() { required className="form-control" onValueChange={onAmountChange} - placeholder={hathorLib.numberUtils.prettyValue(0, decimalPlaces)} />
diff --git a/src/screens/nano-contract/NanoContractExecuteMethod.js b/src/screens/nano-contract/NanoContractExecuteMethod.js index 02025988..aaf3a6bd 100644 --- a/src/screens/nano-contract/NanoContractExecuteMethod.js +++ b/src/screens/nano-contract/NanoContractExecuteMethod.js @@ -9,7 +9,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { t } from 'ttag' import BackButton from '../../components/BackButton'; import InputNumber from '../../components/InputNumber'; -import colors from '../../index.module.scss'; import hathorLib from '@hathor/wallet-lib'; import { useNavigate, useLocation } from 'react-router-dom'; import { get, pullAt } from 'lodash'; @@ -264,8 +263,7 @@ function NanoContractExecuteMethod() { } if (typeToCheck === 'Amount') { - const amountValue = walletUtils.decimalToInteger(value, decimalPlaces); - argValues.push(amountValue); + argValues.push(value); continue; } @@ -282,10 +280,6 @@ function NanoContractExecuteMethod() { // We will skip if the user has just added an empty action continue; } - - const amountValue = isNFT(action.token) ? action.amount : walletUtils.decimalToInteger(action.amount, decimalPlaces); - action.amount = amountValue; - actionsData.push(action); } @@ -377,9 +371,9 @@ function NanoContractExecuteMethod() { return ref.current.value = value} className="form-control output-value" - placeholder={hathorLib.numberUtils.prettyValue(0, decimalPlaces)} /> } @@ -520,21 +514,6 @@ function NanoContractExecuteMethod() { // I start the ref as null, then I set it in the input if it's a withdrawal actionAddressesRef.current[index] = null; - const token = actions[index].token; - // Depending on the token, the input for the amount changes because it might be an NFT - const nft = isNFT(token); - let inputNumberProps; - if (nft) { - inputNumberProps = { - placeholder: '0', - precision: 0, - }; - } else { - inputNumberProps = { - placeholder: hathorLib.numberUtils.prettyValue(0, decimalPlaces) - }; - } - const renderCommon = () => { return (
@@ -549,7 +528,7 @@ function NanoContractExecuteMethod() { requirePositive={true} onValueChange={amount => onActionValueChange(index, 'amount', amount)} className="form-control output-value" - {...inputNumberProps} + isNFT={isNFT(actions[index].token)} />
diff --git a/src/utils/tokens.js b/src/utils/tokens.js index cae65ba3..a76a4368 100644 --- a/src/utils/tokens.js +++ b/src/utils/tokens.js @@ -96,8 +96,7 @@ const tokens = { */ getDepositAmount(mintAmount, depositPercent, decimalPlaces) { if (mintAmount) { - const amountValue = wallet.decimalToInteger(mintAmount, decimalPlaces); - const deposit = hathorLib.tokensUtils.getDepositAmount(amountValue, depositPercent); + const deposit = hathorLib.tokensUtils.getDepositAmount(mintAmount, depositPercent); return hathorLib.numberUtils.prettyValue(deposit, decimalPlaces); } else { return '0'; diff --git a/src/utils/wallet.js b/src/utils/wallet.js index 796cae64..bfefdc2e 100644 --- a/src/utils/wallet.js +++ b/src/utils/wallet.js @@ -614,25 +614,6 @@ const wallet = { return Object.keys(alwaysShowMap); }, - /** - * Converts a decimal value to integer. On the full node and the wallet lib, we only deal with - * integer values for amount. So a value of 45.97 for the user is handled by them as 4597. - * We need the Math.round because of some precision errors in js - * 35.05*100 = 3504.9999999999995 Precision error - * Math.round(35.05*100) = 3505 - * - * @param {number} value The decimal amount - * @param {number} decimalPlaces Number of decimal places - * - * @return {number} Value as an integer - * - * @memberof Wallet - * @inner - */ - decimalToInteger(value, decimalPlaces) { - return Math.round(value*(10**decimalPlaces)); - }, - /** * Returns a string map containing the identifiers for proposals currently being watched. * @returns {Record}