Skip to content
This repository has been archived by the owner on Nov 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #601 from hermeznetwork/refactor-amount-input
Browse files Browse the repository at this point in the history
Refactor amount input
  • Loading branch information
elias-garcia authored Jul 14, 2021
2 parents 5163a5f + 81d8016 commit eaa617d
Show file tree
Hide file tree
Showing 10 changed files with 552 additions and 365 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ REACT_APP_HERMEZ_API_URL=http://localhost:8086
REACT_APP_HERMEZ_CONTRACT_ADDRESS=0x10465b16615ae36F350268eb951d7B0187141D3B
REACT_APP_WITHDRAWAL_DELAYER_CONTRACT_ADDRESS=0x8EEaea23686c319133a7cC110b840d1591d9AeE0
REACT_APP_BATCH_EXPLORER_URL=http://localhost:8080
REACT_APP_ETHERSCAN_URL=https://etherscan.io
REACT_APP_ETHERSCAN_URL=https://etherscan.io
2 changes: 1 addition & 1 deletion src/utils/coordinator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DELAY_TO_NEXT_FORGER } from '../constants'
* @returns Next forgers
*/
function getNextForgers (coordinatorState) {
return coordinatorState.network.nextForgers.reduce((acc, curr) => {
return (coordinatorState.network.nextForgers || []).reduce((acc, curr) => {
const doesItemExist = acc.find(elem => elem.coordinator.forgerAddr === curr.coordinator.forgerAddr)

return doesItemExist
Expand Down
55 changes: 47 additions & 8 deletions src/utils/fees.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { GAS_LIMIT_LOW } from '@hermeznetwork/hermezjs/src/constants'
import { TxType } from '@hermeznetwork/hermezjs/src/enums'
import { getFeeIndex, getFeeValue } from '@hermeznetwork/hermezjs/src/tx-utils'
import { getTokenAmountBigInt, getTokenAmountString } from '@hermeznetwork/hermezjs/src/utils'
import { BigNumber } from 'ethers'
import { parseUnits } from 'ethers/lib/utils'

/**
* Calculates the actual fee that will be paid for a specific transaction
* taking into account the type of transaction, the amount and minimum fee
* @param {Number} amount - The amount of the transaction
* @param {Object} token - The token used in the transaction
* @param {Number} minimumFee - The minimum fee that needs to be payed to the coordinator in token value
* @returns {Number} The real fee that will be paid for this transaction
*/
* Calculates the fee for a L1 deposit into Hermez Network
* @param {Object} token - Token object
* @param {BigNumber} gasPrice - Ethereum gas price
* @returns depositFee
*/
function getDepositFee (token, gasPrice) {
return token.id === 0
? BigNumber.from(GAS_LIMIT_LOW).mul(gasPrice)
: BigNumber.from(0)
}

/**
* Calculates the actual fee that will be paid for a specific transaction
* taking into account the type of transaction, the amount and minimum fee
* @param {Number} amount - The amount of the transaction
* @param {Object} token - The token used in the transaction
* @param {Number} minimumFee - The minimum fee that needs to be payed to the coordinator in token value
* @returns {Number} The real fee that will be paid for this transaction
*/
function getRealFee (amount, token, minimumFee) {
const decimals = token.decimals
const minimumFeeBigInt = getTokenAmountBigInt(minimumFee.toFixed(decimals), decimals).toString()
Expand All @@ -18,4 +34,27 @@ function getRealFee (amount, token, minimumFee) {
return Number(getTokenAmountString(fee, decimals))
}

export { getRealFee }
/**
*
* @param {TxType} txType - Type of the transaction
* @param {BigNumber} amount - Amount to be send in the transaction
* @param {Object} token - Token object
* @param {Number} l2Fee - Transaction fee
* @param {BigNumber} gasPrice - Ethereum gas price
* @returns txFee
*/
function getTransactionFee (txType, amount, token, l2Fee, gasPrice) {
switch (txType) {
case TxType.Deposit: {
return getDepositFee(token, gasPrice)
}
case TxType.ForceExit: {
return BigNumber.from(0)
}
default: {
return parseUnits(getRealFee(amount, token, l2Fee).toString())
}
}
}

export { getDepositFee, getRealFee, getTransactionFee }
80 changes: 79 additions & 1 deletion src/utils/transactions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { BigNumber } from 'ethers'
import { HermezCompressedAmount } from '@hermeznetwork/hermezjs'
import { TxType } from '@hermeznetwork/hermezjs/src/enums'
import { parseUnits } from 'ethers/lib/utils'

import { getMaxAmountFromMinimumFee } from '@hermeznetwork/hermezjs/src/tx-utils'
import { getDepositFee } from './fees'

/**
* Returns the correct amount for a transaction from the Hermez API depending on its type
* @param {Object} transaction - Transaction from the Hermez API
* @returns amount
*/
function getTransactionAmount (transaction) {
if (!transaction) {
return undefined
Expand Down Expand Up @@ -40,7 +51,74 @@ function getTxPendingTime (coordinatorState, isL1, timestamp) {
return timeLeftToForgeInMinutes > 0 ? timeLeftToForgeInMinutes : 0
}

/**
* Checks whether an amount is supported by the compression
* used in the Hermez network
* @param {Number} amount - Selector amount
* @returns {Boolean} Whether it is valid
*/
function isTransactionAmountCompressedValid (amount) {
try {
const compressedAmount = HermezCompressedAmount.compressAmount(amount)
const decompressedAmount = HermezCompressedAmount.decompressAmount(compressedAmount)

return amount.toString() === decompressedAmount.toString()
} catch (e) {
return false
}
}

/**
* Fixes the transaction amount to be sure that it would be supported by Hermez
* @param {BigNumber} amount - Transaction amount to be fixed
* @returns fixedTxAmount
*/
function fixTransactionAmount (amount) {
const fixedTxAmount = HermezCompressedAmount.decompressAmount(
HermezCompressedAmount.floorCompressAmount(amount)
)

return BigNumber.from(fixedTxAmount)
}

/**
* Calculates the max amoumt that can be sent in a transaction
* @param {TxType} txType - Transaction type
* @param {BigNumber} maxAmount - Max amount that can be sent in a transaction (usually it's an account balance)
* @param {Object} token - Token object
* @param {Number} l2Fee - Transaction fee
* @param {BigNumber} gasPrice - Ethereum gas price
* @returns maxTxAmount
*/
function getMaxTxAmount (txType, maxAmount, token, l2Fee, gasPrice) {
const maxTxAmount = (() => {
switch (txType) {
case TxType.ForceExit: {
return maxAmount
}
case TxType.Deposit: {
const depositFee = getDepositFee(token, gasPrice)
const newMaxAmount = maxAmount.sub(depositFee)

return newMaxAmount.gt(0)
? newMaxAmount
: BigNumber.from(0)
}
default: {
const l2FeeBigInt = parseUnits(l2Fee.toFixed(token.decimals), token.decimals)

return BigNumber.from(getMaxAmountFromMinimumFee(l2FeeBigInt, maxAmount).toString())
}
}
})()

return fixTransactionAmount(maxTxAmount)
}

export {
getTransactionAmount,
getTxPendingTime
getTxPendingTime,
isTransactionAmountCompressedValid,
fixTransactionAmount,
getMaxTxAmount
}
179 changes: 179 additions & 0 deletions src/views/shared/amount-input/amount-input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import React from 'react'
import { BigNumber } from 'ethers'

import { getFixedTokenAmount, getTokenAmountInPreferredCurrency } from '../../../utils/currencies'
import { fixTransactionAmount, getMaxTxAmount, isTransactionAmountCompressedValid } from '../../../utils/transactions'
import { parseUnits } from 'ethers/lib/utils'
import { getTransactionFee } from '../../../utils/fees'
import { TxType } from '@hermeznetwork/hermezjs/src/enums'
import { getProvider } from '@hermeznetwork/hermezjs/src/providers'

const INPUT_REGEX = /^\d*(?:\.\d*)?$/

function AmountInput (Component) {
return function (props) {
const { transactionType, account, l2Fee, fiatExchangeRates, preferredCurrency } = props
const [gasPrice, setGasPrice] = React.useState(BigNumber.from(0))
const [value, setValue] = React.useState('')
const [amount, setAmount] = React.useState({ tokens: BigNumber.from(0), fiat: 0 })
const [showInFiat, setShowInFiat] = React.useState(false)
const [isAmountNegative, setIsAmountNegative] = React.useState(undefined)
const [isAmountMoreThanFunds, setIsAmountMoreThanFunds] = React.useState(undefined)
const [isAmountCompressedInvalid, setIsAmountCompressedInvalid] = React.useState(undefined)

React.useEffect(() => {
getProvider().getGasPrice().then((gasPrice) => setGasPrice(gasPrice))
}, [])

React.useEffect(() => {
if (props.onChange) {
const isInvalid = (() => {
if (isAmountNegative === undefined && isAmountMoreThanFunds === undefined && isAmountCompressedInvalid === undefined) {
return undefined
}

return isAmountNegative || isAmountMoreThanFunds || isAmountCompressedInvalid
})()

props.onChange({
amount,
showInFiat,
isInvalid
})
}
}, [amount, showInFiat, isAmountMoreThanFunds, isAmountCompressedInvalid])

/**
* Converts an amount in tokens to fiat. It takes into account the prefered currency
* of the user.
* @param {BigNumber} tokensAmount - Amount to be converted to fiat
* @returns fiatAmount
*/
function convertAmountToFiat (tokensAmount) {
const fixedTokenAmount = getFixedTokenAmount(tokensAmount.toString(), account.token.decimals)

return getTokenAmountInPreferredCurrency(
fixedTokenAmount,
account.token.USD,
preferredCurrency,
fiatExchangeRates
).toFixed(2)
}

/**
* Converts an amount in fiat to tokens.
* @param {Number} fiatAmount - Amount to be converted to tokens
* @returns
*/
function convertAmountToTokens (fiatAmount) {
const tokensAmount = fiatAmount / account.token.USD

return parseUnits(tokensAmount.toFixed(account.token.decimals), account.token.decimals)
}

/**
* Validates the new amount introduced by the user. It checks:
* 1. The amount is not negative.
* 2. The amount + fees doesn't exceed the account balance.
* 3. The amount is supported by Hermez.
* @param {BigNumber} newAmount - New amount to be checked.
*/
function checkAmountValidity (newAmount) {
const fee = getTransactionFee(transactionType, newAmount, account.token, l2Fee, gasPrice)
const newAmountWithFee = newAmount.add(fee)

setIsAmountNegative(newAmountWithFee.lte(BigNumber.from(0)))
setIsAmountMoreThanFunds(newAmountWithFee.gt(BigNumber.from(account.balance)))

if (transactionType !== TxType.Deposit && transactionType !== TxType.ForceExit) {
setIsAmountCompressedInvalid(isTransactionAmountCompressedValid(newAmount) === false)
}
}

/**
* Handles input change events. It's going to check that the input value is a valid
* amount and calculate both the tokens and fiat amounts for a value. It will also
* trigger the validation checks.
* @param {InputEvent} event - Input event
*/
function handleInputChange (event) {
if (INPUT_REGEX.test(event.target.value)) {
if (showInFiat) {
const newAmountInFiat = Number(event.target.value)
const newAmountInTokens = convertAmountToTokens(newAmountInFiat)
const fixedAmountInTokens = fixTransactionAmount(newAmountInTokens)

setAmount({ tokens: fixedAmountInTokens, fiat: newAmountInFiat.toFixed(2) })
checkAmountValidity(fixedAmountInTokens)
setValue(event.target.value)
} else {
try {
const tokensValue = event.target.value.length > 0 ? event.target.value : '0'
const newAmountInTokens = parseUnits(tokensValue, account.token.decimals)
const newAmountInFiat = convertAmountToFiat(newAmountInTokens)

setAmount({ tokens: newAmountInTokens, fiat: newAmountInFiat })
checkAmountValidity(newAmountInTokens)
setValue(event.target.value)
} catch (err) {}
}
}
}

/**
* Handles the "Max" button click. It will calculate the max possible amount that a
* user can send in a transaction based on the account balance. It also takes the fee
* into account (if applicable).
*/
function handleSendAll () {
const maxPossibleAmount = BigNumber.from(account.balance)
const maxAmountWithoutFee = getMaxTxAmount(transactionType, maxPossibleAmount, account.token, l2Fee, gasPrice)
const maxAmountWithoutFeeInFiat = convertAmountToFiat(maxAmountWithoutFee)

if (showInFiat) {
setValue(maxAmountWithoutFeeInFiat)
} else {
const newValue = getFixedTokenAmount(maxAmountWithoutFee, account.token.decimals)

setValue(newValue)
}

setAmount({ tokens: maxAmountWithoutFee, fiat: maxAmountWithoutFeeInFiat })
checkAmountValidity(maxAmountWithoutFee)
}

/**
* Handles the change between tokens and fiat. It will update the value of the input
* with the appropiate value (fiat if the previous value was tokens or tokens if the
* previous value was fiat).
*/
function handleSwapCurrency () {
const newValue = showInFiat
? getFixedTokenAmount(amount.tokens, account.token.decimals)
: amount.fiat

if (value.length > 0) {
setValue(newValue)
}

setShowInFiat(!showInFiat)
}

return (
<Component
value={value}
amount={amount}
showInFiat={showInFiat}
isAmountNegative={isAmountNegative}
isAmountMoreThanFunds={isAmountMoreThanFunds}
isAmountCompressedInvalid={isAmountCompressedInvalid}
onInputChange={handleInputChange}
onSendAll={handleSendAll}
onSwapCurrency={handleSwapCurrency}
{...props}
/>
)
}
}

export default AmountInput
13 changes: 9 additions & 4 deletions src/views/transaction/components/fee/fee.view.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import { TxType } from '@hermeznetwork/hermezjs/src/enums'
import { parseUnits } from 'ethers/lib/utils'

import useFeeStyles from './fee.styles'
import { MAX_TOKEN_DECIMALS } from '../../../../constants'
import { CurrencySymbol, getAmountInPreferredCurrency, getTokenAmountInPreferredCurrency } from '../../../../utils/currencies'
import { CurrencySymbol, getAmountInPreferredCurrency, getFixedTokenAmount, getTokenAmountInPreferredCurrency } from '../../../../utils/currencies'
import { ReactComponent as AngleDownIcon } from '../../../../images/icons/angle-down.svg'
import FeesTable from '../fees-table/fees-table.view'
import { getRealFee } from '../../../../utils/fees'
Expand Down Expand Up @@ -46,8 +46,13 @@ function Fee ({
<div className={classes.feeWrapper}>
<p className={classes.fee}>
Fee&nbsp;
<span>{showInFiat ? Number(l2FeeInFiat) : Number(l2RealFee.toFixed(MAX_TOKEN_DECIMALS))} </span>
<span>{(showInFiat) ? preferredCurrency : token.symbol}</span>
<span>
{
showInFiat
? `${l2FeeInFiat.toFixed(2)} ${preferredCurrency}`
: `${getFixedTokenAmount(parseUnits(l2RealFee.toString(), token.decimals), token.decimals)} ${token.symbol}`
}
</span>
</p>
</div>
)
Expand Down
Loading

0 comments on commit eaa617d

Please sign in to comment.