Skip to content
This repository was archived by the owner on May 24, 2022. It is now read-only.
Merged
117 changes: 78 additions & 39 deletions packages/fether-react/src/Send/TxForm/TxForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -31,14 +33,23 @@ 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 }) => ({
token: tokens[tokenAddress]
}))
@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
Expand All @@ -65,7 +76,6 @@ class TxForm extends Component {
parityStore.api
);
} catch (error) {
console.error(error);
return new BigNumber(-1);
}
}
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
25 changes: 1 addition & 24 deletions packages/fether-react/src/Tokens/TokensList/TokensList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Expand All @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion packages/fether-react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 1 addition & 12 deletions packages/fether-react/src/utils/withAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
},
Expand All @@ -33,8 +23,7 @@ const WithAccount = compose(
account: {
address: accountAddress,
name: accountsInfo[accountAddress].name,
type: accountsInfo[accountAddress].type,
transactionCount
type: accountsInfo[accountAddress].type
},
...otherProps
})
Expand Down
12 changes: 9 additions & 3 deletions packages/fether-react/src/utils/withBalance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
});

/**
Expand Down