Commit 9fb795ce authored by Amaury Martiny's avatar Amaury Martiny Committed by Luke Schoen
Browse files

refactor: Don't call transactionCountOf$ until needed (#414)

* Show parity/light.js's logs too

* Remove txCount from withAccount

* withBalance shows component immeidately

* Play around

* Make it work

* Cleaner code

* Remove 'Loading account tokens...' modal

* Re-order import

* Optimize code

* Fix regression

* Fix typo

* Luke's grumbles

* Put "checking..." when fetching async values

* Show gas error on amount field
parent ac3fe778
Pipeline #33963 passed with stages
in 9 minutes and 23 seconds
......@@ -5,9 +5,9 @@
import React, { Component } from 'react';
import BigNumber from 'bignumber.js';
import { chainId$, transactionCountOf$ } from '@parity/light.js';
import { Clickable, Form as FetherForm, Header } from 'fether-ui';
import createDecorator from 'final-form-calculate';
import { chainId$ } from '@parity/light.js';
import debounce from 'debounce-promise';
import { Field, Form } from 'react-final-form';
import { fromWei, toWei } from '@parity/api/lib/util/wei';
......@@ -16,9 +16,11 @@ import { isAddress } from '@parity/api/lib/util/address';
import light from '@parity/light.js-react';
import { Link } from 'react-router-dom';
import { OnChange } from 'react-final-form-listeners';
import { startWith } from 'rxjs/operators';
import { withProps } from 'recompose';
import { estimateGas } from '../../utils/transaction';
import Debug from '../../utils/debug';
import RequireHealthOverlay from '../../RequireHealthOverlay';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import TxDetails from './TxDetails';
......@@ -31,6 +33,8 @@ const MEDIUM_AMOUNT_MAX_CHARS = 14;
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
const debug = Debug('TxForm');
@inject('parityStore', 'sendStore')
@withTokens
@withProps(({ match: { params: { tokenAddress } }, tokens }) => ({
......@@ -38,7 +42,14 @@ const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
}))
@withAccount
@light({
chainId: () => chainId$()
// We need to wait for 3 values that might take time:
// - ethBalance: to check that we have enough to send amount+fees
// - chainId & transactionCount: needed to construct the tx
// For the three of them, we add the `startWith()` operator so that the UI is
// not blocked while waiting for their first response.
chainId: () => chainId$().pipe(startWith(undefined)),
transactionCount: ({ account: { address } }) =>
transactionCountOf$(address).pipe(startWith(undefined))
})
@withBalance // Balance of current token (can be ETH)
@withEthBalance // ETH balance
......@@ -65,7 +76,6 @@ class TxForm extends Component {
parityStore.api
);
} catch (error) {
console.error(error);
return new BigNumber(-1);
}
}
......@@ -130,14 +140,13 @@ class TxForm extends Component {
handleSubmit = values => {
const {
account: { address, type, transactionCount },
chainId,
account: { address, type },
history,
sendStore,
token
} = this.props;
sendStore.setTx({ ...values, chainId, token, transactionCount });
sendStore.setTx({ ...values, token });
if (type === 'signer') {
history.push(`/send/${token.address}/from/${address}/txqrcode`);
......@@ -184,8 +193,11 @@ class TxForm extends Component {
render () {
const {
account: { address, type },
chainId,
ethBalance,
sendStore: { tx },
token
token,
transactionCount
} = this.props;
const { showDetails } = this.state;
......@@ -208,13 +220,24 @@ class TxForm extends Component {
decimals={6}
drawers={[
<Form
decorators={[this.decorator]}
initialValues={{
chainId,
ethBalance,
from: address,
gasPrice: 4,
transactionCount,
...tx
}}
keepDirtyOnReinitialize // Don't erase other fields when we get new initialValues
key='txForm'
initialValues={{ from: address, gasPrice: 4, ...tx }}
mutators={{
recalculateMax: this.recalculateMax
}}
onSubmit={this.handleSubmit}
validate={this.validateForm}
decorators={[this.decorator]}
mutators={{ recalculateMax: this.recalculateMax }}
render={({
errors,
handleSubmit,
valid,
validating,
......@@ -223,6 +246,16 @@ class TxForm extends Component {
}) => (
<form className='send-form' onSubmit={handleSubmit}>
<fieldset className='form_fields'>
{/* Unfortunately, we need to set these hidden fields
for the 3 values that come from props, even
though they are already set in initialValues. */}
<Field name='chainId' render={this.renderNull} />
<Field name='ethBalance' render={this.renderNull} />
<Field
name='transactionCount'
render={this.renderNull}
/>
<Field
as='textarea'
autoFocus
......@@ -314,7 +347,11 @@ class TxForm extends Component {
disabled={!valid || validating}
className='button'
>
{validating
{validating ||
errors.chainId ||
errors.ethBalance ||
errors.gas ||
errors.transactionCount
? 'Checking...'
: type === 'signer'
? 'Scan'
......@@ -335,6 +372,11 @@ class TxForm extends Component {
);
}
renderNull = () => null;
/**
* Prevalidate form on user's input. These validations are sync.
*/
preValidate = values => {
const { balance, token } = this.props;
......@@ -389,60 +431,57 @@ class TxForm extends Component {
}
try {
const {
account: { address, transactionCount },
chainId,
ethBalance,
token
} = this.props;
if (!chainId) {
throw new Error('chaindId is required for an EthereumTx');
}
const { token } = this.props;
if (!address) {
throw new Error('address of an account is required');
}
const preValidation = this.preValidate(values);
if (!transactionCount) {
throw new Error('transactionCount is required for an EthereumTx');
// preValidate return an error if a field isn't valid
if (preValidation !== true) {
return preValidation;
}
if (!token || !token.address || !token.decimals) {
throw new Error('token information is required for an EthereumTx');
// The 3 values below (`chainId`, `ethBalance`, and `transactionCount`)
// come from props, and are passed into `values` via the form's
// initialValues. As such, they don't have visible fields, so these
// errors won't actually be shown on the UI.
if (!values.chainId) {
debug('Fetching chain ID');
return { chainId: 'Fetching chain ID' };
}
if (!ethBalance) {
throw new Error('No "ethBalance"');
if (!values.ethBalance) {
debug('Fetching Ether balance');
return { ethBalance: 'Fetching Ether balance' };
}
const preValidation = this.preValidate(values);
// preValidate return an error if a field isn't valid
if (preValidation !== true) {
return preValidation;
if (!values.transactionCount) {
debug('Fetching transaction count for nonce');
return { transactionCount: 'Fetching transaction count for nonce' };
}
if (values.gas && values.gas.eq(-1)) {
debug('Unable to estimate gas...');
// Show this error on the `amount` field
return { amount: 'Unable to estimate gas...' };
}
// If the gas hasn't been calculated yet, then we don't show any errors,
// just wait a bit more
if (!this.isEstimatedTxFee(values)) {
return { amount: 'Estimating gas...' };
debug('Estimating gas...');
return { gas: 'Estimating gas...' };
}
// Verify that `gas + (eth amount if sending eth) <= ethBalance`
if (
this.estimatedTxFee(values)
.plus(token.address === 'ETH' ? toWei(values.amount) : 0)
.gt(toWei(ethBalance))
.gt(toWei(values.ethBalance))
) {
return token.address !== 'ETH'
? { amount: 'ETH balance too low to pay for gas' }
: { amount: "You don't have enough ETH balance" };
}
debug('Transaction seems valid');
} catch (err) {
console.error(err);
return {
......
......@@ -18,18 +18,9 @@ import withBalance from '../../../utils/withBalance';
@inject('sendStore')
class TokenBalance extends Component {
static propTypes = {
hideLoadingAccountTokensModal: PropTypes.func,
token: PropTypes.object
};
componentDidMount () {
const { hideLoadingAccountTokensModal } = this.props;
if (hideLoadingAccountTokensModal) {
hideLoadingAccountTokensModal();
}
}
handleClick = () => {
const {
account: { address },
......
......@@ -4,37 +4,19 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import { Modal } from 'fether-ui';
import RequireHealthOverlay from '../../RequireHealthOverlay';
import TokenBalance from './TokenBalance';
import withTokens from '../../utils/withTokens';
import loading from '../../assets/img/icons/loading.svg';
@withTokens
class TokensList extends Component {
state = {
isLoadingAccountTokens: true
};
hideLoadingAccountTokensModal = () => {
this.setState({ isLoadingAccountTokens: false });
};
render () {
const { tokensArray } = this.props;
const { isLoadingAccountTokens } = this.state;
// Show empty token placeholder if tokens have not been loaded yet
const shownArray = tokensArray.length ? tokensArray : [{}];
return (
<RequireHealthOverlay require='sync'>
<Modal
description='Please wait...'
fullscreen={false}
icon={loading}
title='Loading account tokens...'
visible={isLoadingAccountTokens}
/>
<div className='window_content'>
<div className='box -scroller'>
<ul className='list -padded'>
......@@ -43,12 +25,7 @@ class TokensList extends Component {
index // With empty tokens, the token.address is not defined, so we prefix with index
) => (
<li key={`${index}-${token.address}`}>
<TokenBalance
hideLoadingAccountTokensModal={
this.hideLoadingAccountTokensModal
}
token={token}
/>
<TokenBalance token={token} />
</li>
))}
</ul>
......
......@@ -16,7 +16,7 @@ import rootStore from './stores';
import './index.css';
// Show debug logs
window.localStorage.debug = 'fether*'; // https://github.com/visionmedia/debug#browser-support
window.localStorage.debug = 'fether*,@parity*'; // https://github.com/visionmedia/debug#browser-support
// Set recompose to use RxJS
// https://github.com/acdlite/recompose/blob/master/docs/API.md#setobservableconfig
......
......@@ -6,24 +6,14 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import { compose, mapProps } from 'recompose';
import { startWith } from 'rxjs/operators';
import light from '@parity/light.js-react';
import { transactionCountOf$ } from '@parity/light.js';
import withAccountsInfo from '../utils/withAccountsInfo';
const WithAccount = compose(
withRouter,
withAccountsInfo,
light({
transactionCount: props =>
transactionCountOf$(props.match.params.accountAddress).pipe(
startWith(undefined)
)
}),
mapProps(
({
transactionCount,
match: {
params: { accountAddress }
},
......@@ -33,8 +23,7 @@ const WithAccount = compose(
account: {
address: accountAddress,
name: accountsInfo[accountAddress].name,
type: accountsInfo[accountAddress].type,
transactionCount
type: accountsInfo[accountAddress].type
},
...otherProps
})
......
......@@ -9,19 +9,25 @@ import branch from 'recompose/branch';
import compose from 'recompose/compose';
import { fromWei } from '@parity/api/lib/util/wei';
import light from '@parity/light.js-react';
import { map } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';
import withProps from 'recompose/withProps';
export const withErc20Balance = light({
erc20Balance: ({ token, account: { address } }) =>
makeContract(token.address, abi)
.balanceOf$(address)
.pipe(map(value => value && value.div(10 ** token.decimals)))
.pipe(
map(value => value && value.div(10 ** token.decimals)),
startWith(undefined)
)
});
export const withEthBalance = light({
ethBalance: ({ account: { address } }) =>
balanceOf$(address).pipe(map(value => value && fromWei(value)))
balanceOf$(address).pipe(
map(value => value && fromWei(value)),
startWith(undefined)
)
});
/**
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment