diff --git a/package-lock.json b/package-lock.json index a1b691319..b8c261e7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1628,9 +1628,9 @@ } }, "@hermeznetwork/hermezjs": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@hermeznetwork/hermezjs/-/hermezjs-1.0.9.tgz", - "integrity": "sha512-9OVaVsZP36XN/hIV7mCLbXb7klwPwLS+ivzjeProftCu780bQja+7HGuHT0pOyd62R0aKlHeWtBjKyG2VNIjvw==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@hermeznetwork/hermezjs/-/hermezjs-1.0.10.tgz", + "integrity": "sha512-2wDdfauvRwu4PfMHNUVf3xjEqbPJfCEFxYJyo54HWXm0muSXQ0s0NfJgxzhZdGeFuUuUHNcAtpUEhDC3VqChOA==", "requires": { "@babel/polyfill": "^7.12.1", "@ethersproject/keccak256": "^5.0.7", diff --git a/package.json b/package.json index 8afa6b0ae..5d22ed485 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint:fix": "standard --fix" }, "dependencies": { - "@hermeznetwork/hermezjs": "^1.0.9", + "@hermeznetwork/hermezjs": "^1.0.10", "axios": "^0.21.1", "big-integer": "^1.6.48", "clsx": "^1.1.1", diff --git a/src/store/transaction/transaction.actions.js b/src/store/transaction/transaction.actions.js index 012db393f..71268e7c5 100644 --- a/src/store/transaction/transaction.actions.js +++ b/src/store/transaction/transaction.actions.js @@ -20,6 +20,9 @@ export const transactionActionTypes = { LOAD_ACCOUNTS: '[TRANSACTION] LOAD ACCOUNTS', LOAD_ACCOUNTS_SUCCESS: '[TRANSACTION] LOAD ACCOUNTS SUCCESS', LOAD_ACCOUNTS_FAILURE: '[TRANSACTION] LOAD ACCOUNTS FAILURE', + LOAD_ESTIMATED_WITHDRAW_FEE: '[TRANSACTION] LOAD ESTIMATED WITHDRAW FEE', + LOAD_ESTIMATED_WITHDRAW_FEE_SUCCESS: '[TRANSACTION] LOAD ESTIMATED WITHDRAW FEE SUCCESS', + LOAD_ESTIMATED_WITHDRAW_FEE_FAILURE: '[TRANSACTION] LOAD ESTIMATED WITHDRAW FEE FAILURE', START_TRANSACTION_SIGNING: '[TRANSACTION] START TRANSACTION SIGNING', STOP_TRANSACTION_SIGNING: '[TRANSACTION] STOP TRANSACTION SIGNING', RESET_STATE: '[TRANSACTION] RESET STATE' @@ -166,6 +169,26 @@ function loadFeesFailure (error) { } } +function loadEstimatedWithdrawFee () { + return { + type: transactionActionTypes.LOAD_ESTIMATED_WITHDRAW_FEE + } +} + +function loadEstimatedWithdrawFeeSuccess (estimatedFee) { + return { + type: transactionActionTypes.LOAD_ESTIMATED_WITHDRAW_FEE_SUCCESS, + estimatedFee + } +} + +function loadEstimatedWithdrawFeeFailure (error) { + return { + type: transactionActionTypes.LOAD_ESTIMATED_WITHDRAW_FEE_FAILURE, + error + } +} + function startTransactionSigning () { return { type: transactionActionTypes.START_TRANSACTION_SIGNING @@ -206,6 +229,9 @@ export { loadFees, loadFeesSuccess, loadFeesFailure, + loadEstimatedWithdrawFee, + loadEstimatedWithdrawFeeSuccess, + loadEstimatedWithdrawFeeFailure, startTransactionSigning, stopTransactionSigning, resetState diff --git a/src/store/transaction/transaction.reducer.js b/src/store/transaction/transaction.reducer.js index 1e946f21a..4d2d8cdc9 100644 --- a/src/store/transaction/transaction.reducer.js +++ b/src/store/transaction/transaction.reducer.js @@ -34,6 +34,9 @@ const initialTransactionState = { }, [STEP_NAME.REVIEW_TRANSACTION]: { transaction: undefined, + estimatedWithdrawFeeTask: { + status: 'pending' + }, isTransactionBeingSigned: false } } @@ -290,6 +293,50 @@ function transactionReducer (state = initialTransactionState, action) { } } } + case transactionActionTypes.LOAD_ESTIMATED_WITHDRAW_FEE: { + return { + ...state, + steps: { + ...state.steps, + [STEP_NAME.REVIEW_TRANSACTION]: { + ...state.steps[STEP_NAME.REVIEW_TRANSACTION], + estimatedWithdrawFeeTask: { + status: 'loading' + } + } + } + } + } + case transactionActionTypes.LOAD_ESTIMATED_WITHDRAW_FEE_SUCCESS: { + return { + ...state, + steps: { + ...state.steps, + [STEP_NAME.REVIEW_TRANSACTION]: { + ...state.steps[STEP_NAME.REVIEW_TRANSACTION], + estimatedWithdrawFeeTask: { + status: 'successful', + data: action.estimatedFee + } + } + } + } + } + case transactionActionTypes.LOAD_ESTIMATED_WITHDRAW_FEE_FAILURE: { + return { + ...state, + steps: { + ...state.steps, + [STEP_NAME.REVIEW_TRANSACTION]: { + ...state.steps[STEP_NAME.REVIEW_TRANSACTION], + estimatedWithdrawFeeTask: { + status: 'failed', + error: action.error + } + } + } + } + } case transactionActionTypes.START_TRANSACTION_SIGNING: { return { ...state, diff --git a/src/store/transaction/transaction.thunks.js b/src/store/transaction/transaction.thunks.js index 1237917cc..dcf29c206 100644 --- a/src/store/transaction/transaction.thunks.js +++ b/src/store/transaction/transaction.thunks.js @@ -1,12 +1,15 @@ -import { CoordinatorAPI, Tx, HermezCompressedAmount } from '@hermeznetwork/hermezjs' +import { CoordinatorAPI, Tx, TxFees, HermezCompressedAmount } from '@hermeznetwork/hermezjs' import { TxType, TxState } from '@hermeznetwork/hermezjs/src/enums' import { getPoolTransactions } from '@hermeznetwork/hermezjs/src/tx-pool' +import * as ethers from 'ethers' import * as transactionActions from './transaction.actions' import * as globalThunks from '../global/global.thunks' import * as ethereum from '../../utils/ethereum' import { getAccountBalance } from '../../utils/accounts' import { getFixedTokenAmount, getTokenAmountInPreferredCurrency } from '../../utils/currencies' +import { getProvider } from '@hermeznetwork/hermezjs/dist/node/providers' +import { ETHER_TOKEN_ID } from '@hermeznetwork/hermezjs/dist/node/constants' /** * Fetches the account details for a token id in MetaMask. @@ -186,6 +189,28 @@ function fetchFees () { } } +function fetchEstimatedWithdrawFee (token, amount) { + return async (dispatch, getState) => { + dispatch(transactionActions.loadEstimatedWithdrawFee()) + + try { + const { global: { signer } } = getState() + const provider = getProvider() + const gasPrice = await provider.getGasPrice() + const estimatedMerkleSiblingsLength = 4 + const overrides = { gasPrice } + const gasLimit = await TxFees.estimateWithdrawGasLimit(token, estimatedMerkleSiblingsLength, amount, overrides, signer) + const feeBigInt = BigInt(gasLimit) * BigInt(gasPrice) + const ethToken = await CoordinatorAPI.getToken(ETHER_TOKEN_ID) + const fee = Number(ethers.utils.formatEther(feeBigInt)) * ethToken.USD + + dispatch(transactionActions.loadEstimatedWithdrawFeeSuccess(fee)) + } catch (err) { + dispatch(transactionActions.loadEstimatedWithdrawFeeFailure(err)) + } + } +} + function deposit (amount, account) { return (dispatch, getState) => { const { global: { wallet, signer } } = getState() @@ -376,6 +401,7 @@ export { fetchPoolTransactions, fetchAccounts, fetchFees, + fetchEstimatedWithdrawFee, deposit, withdraw, forceExit, diff --git a/src/views/transaction/components/transaction-overview/transaction-overview.styles.js b/src/views/transaction/components/transaction-overview/transaction-overview.styles.js index 34f4d8f94..f6fa1e4d2 100644 --- a/src/views/transaction/components/transaction-overview/transaction-overview.styles.js +++ b/src/views/transaction/components/transaction-overview/transaction-overview.styles.js @@ -37,6 +37,33 @@ const useTransactionOverviewStyles = createUseStyles(theme => ({ lineHeight: 1.75, marginTop: theme.spacing(4), textAlign: 'center' + }, + exitInfoWrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + marginTop: theme.spacing(3) + }, + exitHelperTextWrapper: { + display: 'flex', + alignItems: 'center', + background: theme.palette.grey.light, + padding: `${theme.spacing(1)}px ${theme.spacing(1.5)}px`, + borderRadius: 50 + }, + exitHelperIcon: { + fill: theme.palette.grey.main, + marginRight: theme.spacing(1) + }, + exitHelperText: { + color: theme.palette.grey.main, + fontWeight: theme.fontWeights.medium + }, + exitEstimatedFeeHelperText: { + marginTop: theme.spacing(2), + color: theme.palette.grey.main, + fontWeight: theme.fontWeights.medium } })) diff --git a/src/views/transaction/components/transaction-overview/transaction-overview.view.jsx b/src/views/transaction/components/transaction-overview/transaction-overview.view.jsx index 05c32f0db..116e6dbaa 100644 --- a/src/views/transaction/components/transaction-overview/transaction-overview.view.jsx +++ b/src/views/transaction/components/transaction-overview/transaction-overview.view.jsx @@ -6,7 +6,7 @@ import { getFeeIndex, getFeeValue } from '@hermeznetwork/hermezjs/src/tx-utils' import { getTokenAmountBigInt, getTokenAmountString } from '@hermeznetwork/hermezjs/src/utils' import useTransactionOverviewStyles from './transaction-overview.styles' -import { CurrencySymbol, getTokenAmountInPreferredCurrency, getFixedTokenAmount } from '../../../../utils/currencies' +import { CurrencySymbol, getTokenAmountInPreferredCurrency, getFixedTokenAmount, getAmountInPreferredCurrency } from '../../../../utils/currencies' import TransactionInfo from '../../../shared/transaction-info/transaction-info.view' import Container from '../../../shared/container/container.view' import FiatAmount from '../../../shared/fiat-amount/fiat-amount.view' @@ -14,6 +14,7 @@ import TokenBalance from '../../../shared/token-balance/token-balance.view' import Spinner from '../../../shared/spinner/spinner.view' import FormButton from '../../../shared/form-button/form-button.view' import { MAX_TOKEN_DECIMALS } from '../../../../constants' +import { ReactComponent as InfoIcon } from '../../../../images/icons/info.svg' function TransactionOverview ({ wallet, @@ -26,8 +27,10 @@ function TransactionOverview ({ instantWithdrawal, completeDelayedWithdrawal, account, + estimatedWithdrawFeeTask, preferredCurrency, fiatExchangeRates, + onLoadEstimatedWithdrawFee, onDeposit, onForceExit, onWithdraw, @@ -38,6 +41,12 @@ function TransactionOverview ({ const classes = useTransactionOverviewStyles() const [isButtonDisabled, setIsButtonDisabled] = React.useState(false) + React.useEffect(() => { + if (transactionType === TxType.Exit) { + onLoadEstimatedWithdrawFee(account.token, amount) + } + }, [transactionType, account, amount]) + /** * Converts the transaction amount to fiat in the preferred currency * @@ -58,6 +67,12 @@ function TransactionOverview ({ ) } + function getEstimatedWithdrawFee () { + return estimatedWithdrawFeeTask.status === 'successful' + ? getAmountInPreferredCurrency(estimatedWithdrawFeeTask.data, preferredCurrency, fiatExchangeRates).toFixed(2) + : '--' + } + /** * Converts the transaction type to a readable button label * @@ -70,7 +85,7 @@ function TransactionOverview ({ case TxType.Transfer: return 'Send' case TxType.Exit: - return 'Withdraw' + return 'Initiate withdraw' case TxType.Withdraw: return 'Withdraw' case TxType.ForceExit: @@ -171,6 +186,21 @@ function TransactionOverview ({ /> ) } + { + transactionType === TxType.Exit && ( +
+
+ +

+ This step is not reversible. Once initiated, the withdrawal process must be completed. +

+
+

+ The estimated fee for step 2 of the withdrawal process is {getEstimatedWithdrawFee()} {CurrencySymbol[preferredCurrency].symbol} +

+
+ ) + } diff --git a/src/views/transaction/transaction.view.jsx b/src/views/transaction/transaction.view.jsx index 7306c2f90..31a819690 100644 --- a/src/views/transaction/transaction.view.jsx +++ b/src/views/transaction/transaction.view.jsx @@ -50,6 +50,7 @@ function Transaction ({ onGoToBuildTransactionStep, onGoToTransactionOverviewStep, onFinishTransaction, + onLoadEstimatedWithdrawFee, onDeposit, onForceExit, onWithdraw, @@ -192,6 +193,8 @@ function Transaction ({ exit={stepData.transaction.exit} instantWithdrawal={instantWithdrawal} completeDelayedWithdrawal={completeDelayedWithdrawal} + estimatedWithdrawFeeTask={stepData.estimatedWithdrawFeeTask} + onLoadEstimatedWithdrawFee={onLoadEstimatedWithdrawFee} onDeposit={onDeposit} onForceExit={onForceExit} onWithdraw={onWithdraw} @@ -355,6 +358,9 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(push('/')) } }, + onLoadEstimatedWithdrawFee: (token, amount) => { + dispatch(transactionThunks.fetchEstimatedWithdrawFee(token, amount)) + }, onDeposit: (amount, account) => dispatch(transactionThunks.deposit(amount, account)), onForceExit: (amount, account) =>