From a1e7aad5ec981559085d52ea2c615e1998514a9a Mon Sep 17 00:00:00 2001 From: Amaury Martiny Date: Thu, 21 Jun 2018 17:03:22 +0200 Subject: [PATCH 1/4] Change how txForm works (fix #72) --- packages/light-react/package.json | 3 +- .../light-react/src/Send/TxForm/TxForm.js | 122 +++++++++++----- .../src/assets/sass/shared/_button.scss | 1 + packages/light-react/src/stores/sendStore.js | 130 +++++++----------- packages/light-ui/src/FormField/FormField.js | 9 +- yarn.lock | 6 +- 6 files changed, 144 insertions(+), 127 deletions(-) diff --git a/packages/light-react/package.json b/packages/light-react/package.json index 11d54f77..9e2ee977 100644 --- a/packages/light-react/package.json +++ b/packages/light-react/package.json @@ -38,7 +38,7 @@ "@parity/api": "^2.1.22", "@parity/light.js": "https://github.com/parity-js/light.js#9646ce15d9dd9c4cf11776ddd613d5bd86016f94", "@parity/shared": "^3.0.2", - "bignumber.js": "^7.2.1", + "bignumber.js": "^4.1.0", "is-electron": "^2.1.0", "light-hoc": "^0.1.0", "light-ui": "^0.1.0", @@ -50,6 +50,7 @@ "react-dom": "^16.3.2", "react-router-dom": "^4.2.2", "react-scripts": "1.1.4", + "react-tooltip": "^3.6.1", "rxjs": "^6.2.0" }, "devDependencies": { diff --git a/packages/light-react/src/Send/TxForm/TxForm.js b/packages/light-react/src/Send/TxForm/TxForm.js index 3c4d90b2..a0a7a285 100644 --- a/packages/light-react/src/Send/TxForm/TxForm.js +++ b/packages/light-react/src/Send/TxForm/TxForm.js @@ -4,10 +4,13 @@ // SPDX-License-Identifier: MIT import React, { Component } from 'react'; +import debounce from 'lodash/debounce'; import { FormField, Header } from 'light-ui'; import { fromWei, toWei } from '@parity/api/lib/util/wei'; import { inject, observer } from 'mobx-react'; +import { isAddress } from '@parity/api/lib/util/address'; import { Link } from 'react-router-dom'; +import ReactTooltip from 'react-tooltip'; import TokenBalance from '../../Tokens/TokensList/TokenBalance'; import withBalance from '../../utils/withBalance'; @@ -19,42 +22,54 @@ const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei @withBalance(({ sendStore: { token } }) => token) @observer class Send extends Component { - componentDidMount () { - this.props.sendStore.estimateGas(); + state = { + amount: '', // In Ether or in token + gasPrice: 4, // in Gwei + to: '', + ...this.props.sendStore.tx + }; + + componentDidUpdate () { + if (!this.hasErrors()) { + const { amount, gasPrice, to } = this.state; + this.props.sendStore.setTx({ amount, gasPrice, to }); + this.estimateGas(); + } } - getMaxAmount = () => { + estimateGas = debounce(() => { + this.props.sendStore.estimateGas(); + }, 1000); + + static getDerivedStateFromProps (nextProps, prevState) { const { balance, - sendStore: { estimated, tx } - } = this.props; - - // TODO this in sendStore as @computed? - return balance && estimated - ? +fromWei( - toWei(balance).minus( - estimated.multipliedBy(toWei(tx.gasPrice, 'shannon')) - ) - ) - : 0.01; - }; + sendStore: { estimated } + } = nextProps; + + return { + maxAmount: + balance && estimated + ? +fromWei( + toWei(balance).minus( + estimated.mul(toWei(prevState.gasPrice, 'shannon')) + ) + ) + : 0.01 + }; + } handleChangeAmount = ({ target: { value } }) => - this.props.sendStore.setTxAmount(value); + this.setState({ amount: value }); handleChangeGasPrice = ({ target: { value } }) => - this.props.sendStore.setTxGasPrice(value); + this.setState({ gasPrice: value }); handleChangeTo = ({ target: { value } }) => { - const { sendStore } = this.props; - sendStore.setTxTo(value); - // Estimate the gas to this address, if we're sending ETH. - if (sendStore.tokenAddress === 'ETH') { - sendStore.estimateGas(); - } + this.setState({ to: value }); }; - handleMax = () => this.props.sendStore.setTxAmount(this.getMaxAmount()); + handleMax = () => this.setState({ amount: this.state.maxAmount }); handleSubmit = e => { e.preventDefault(); @@ -67,10 +82,39 @@ class Send extends Component { history.push('/send/signer'); }; + /** + * Get form errors. + * + * TODO Use a React form library to do this? + */ + hasErrors = () => { + const { amount, maxAmount, to } = this.state; + if (!amount || isNaN(amount)) { + return 'Please enter a valid amount'; + } + + if (amount < 0) { + return 'Please enter a positive amount '; + } + + if (amount > maxAmount) { + return "You don't have enough balance"; + } + + if (!isAddress(to)) { + return 'Please enter a valid Ethereum address'; + } + + return null; + }; + render () { const { - sendStore: { token, tx } + sendStore: { token } } = this.props; + const { amount, gasPrice, maxAmount, to } = this.state; + + const errors = this.hasErrors(); return (
@@ -99,14 +143,14 @@ class Send extends Component {
]} @@ -173,6 +219,12 @@ class Send extends Component { />
+ ); } diff --git a/packages/light-react/src/assets/sass/shared/_button.scss b/packages/light-react/src/assets/sass/shared/_button.scss index f03bf5cf..c5932197 100644 --- a/packages/light-react/src/assets/sass/shared/_button.scss +++ b/packages/light-react/src/assets/sass/shared/_button.scss @@ -24,6 +24,7 @@ border-color: $faint; background-color: $faint; color: $grey; + pointer-events: none; } &.-tiny { diff --git a/packages/light-react/src/stores/sendStore.js b/packages/light-react/src/stores/sendStore.js index fea2d993..da4d4527 100644 --- a/packages/light-react/src/stores/sendStore.js +++ b/packages/light-react/src/stores/sendStore.js @@ -7,7 +7,7 @@ import abi from '@parity/shared/lib/contracts/abi/eip20'; import { action, computed, observable } from 'mobx'; import { BigNumber } from 'bignumber.js'; import { blockNumber$, makeContract$, post$ } from '@parity/light.js'; -import { isAddress } from '@parity/api/lib/util/address'; +import memoize from 'lodash/memoize'; import noop from 'lodash/noop'; import { toWei } from '@parity/api/lib/util/wei'; @@ -15,17 +15,13 @@ import parityStore from './parityStore'; import tokensStore from './tokensStore'; const DEFAULT_GAS = new BigNumber(21000); // Default gas amount to show +const GAS_MULT_FACTOR = 1.2; // Since estimateGas is not always accurate, we add a 120% factor for buffer. class SendStore { @observable blockNumber; // Current block number, used to calculate tx confirmations. @observable estimated = DEFAULT_GAS; // Estimated gas amount for this transaction. @observable tokenAddress; // 'ETH', or the token contract address - @observable - tx = { - amount: '', // In Ether or in token - gasPrice: 4, // in Gwei - to: '' - }; // The actual tx we are sending. No need to be observable. + tx = {}; // The actual tx we are sending. No need to be observable. @observable txStatus; // Status of the tx, see wiki for details. constructor () { @@ -84,47 +80,39 @@ class SendStore { * Estimate the amount of gas for our transaction. */ estimateGas = () => { - if (!this.isTxValid) { - return Promise.resolve(DEFAULT_GAS); - } - if (this.tokenAddress === 'ETH') { - return this.api.eth - .estimateGas(this.txForEth) - .then(this.setEstimated) - .catch(noop); + return this.estimateGasForEth(this.txForEth); } else { - return this.contract.contractObject.instance.transfer - .estimateGas(this.txForErc20.options, this.txForErc20.args) - .then(this.setEstimated) - .catch(noop); + return this.estimateGasForErc20(this.txForErc20); } }; - @computed - get isTxValid () { - if ( - !this.tx || // There should be a tx - !isAddress(this.tx.to) || // The address should be okay - !this.tx.amount || - isNaN(this.tx.amount) || - isNaN(this.tx.gasPrice) - ) { - return false; - } + /** + * Estimate gas to transfer in ERC20 contract. Expensive function, so we + * memoize it. + */ + estimateGasForErc20 = memoize(txForErc20 => { + return this.contract.contractObject.instance.transfer + .estimateGas(txForErc20.options, txForErc20.args) + .then(this.setEstimated) + .catch(noop); + }, JSON.stringify); - return true; - } + /** + * Estimate gas to transfer to an ETH address. Expensive function, so we + * memoize it. + */ + estimateGasForEth = memoize(txForEth => { + return this.api.eth + .estimateGas(txForEth) + .then(this.setEstimated) + .catch(noop); + }, JSON.stringify); /** * Create a transaction. */ send = () => { - if (!this.isTxValid) { - console.error('Transaction is invalid.', this.tx); - return; - } - const send$ = this.tokenAddress === 'ETH' ? post$(this.txForEth) @@ -156,37 +144,16 @@ class SendStore { return tokensStore.tokens[this.tokenAddress]; } - /** - * This.tx is a user-friendly tx object. We convert it now as it can be - * passed to post$(tx). - */ - @computed - get txForEth () { - if (!this.isTxValid) { - return {}; - } - - return { - gasPrice: toWei(this.tx.gasPrice, 'shannon'), // shannon == gwei - to: this.tx.to, - value: toWei(this.tx.amount.toString()) - }; - } - /** * This.tx is a user-friendly tx object. We convert it now as it can be * passed to makeContract$(...). */ @computed get txForErc20 () { - if (!this.isTxValid) { - return {}; - } - return { args: [ this.tx.to, - new BigNumber(this.tx.amount).multipliedBy( + new BigNumber(this.tx.amount).mul( new BigNumber(10).pow(this.token.decimals) ) ], @@ -196,42 +163,37 @@ class SendStore { }; } - @action - setTokenAddress = tokenAddress => { - this.tokenAddress = tokenAddress; - }; - - @action - setTx = tx => { - this.tx = tx; - }; - - @action - setTxAmount = amount => { - this.tx.amount = amount; - }; + /** + * This.tx is a user-friendly tx object. We convert it now as it can be + * passed to post$(tx). + */ + @computed + get txForEth () { + return { + gasPrice: toWei(this.tx.gasPrice, 'shannon'), // shannon == gwei + to: this.tx.to, + value: toWei(this.tx.amount.toString()) + }; + } @action - setTxGasPrice = gasPrice => { - this.tx.gasPrice = gasPrice; + setBlockNumber = blockNumber => { + this.blockNumber = blockNumber; }; @action - setTxTo = to => { - this.tx.to = to; + setEstimated = estimated => { + this.estimated = estimated.mul(GAS_MULT_FACTOR); }; @action - setBlockNumber = blockNumber => { - this.blockNumber = blockNumber; + setTokenAddress = tokenAddress => { + this.tokenAddress = tokenAddress; }; @action - setEstimated = estimated => { - // Since estimateGas is not always accurate, we add a 120% factor for buffer. - const GAS_MULT_FACTOR = 1.2; - - this.estimated = estimated.multipliedBy(GAS_MULT_FACTOR); + setTx = tx => { + this.tx = tx; }; @action diff --git a/packages/light-ui/src/FormField/FormField.js b/packages/light-ui/src/FormField/FormField.js index e1e33719..b2f75e82 100644 --- a/packages/light-ui/src/FormField/FormField.js +++ b/packages/light-ui/src/FormField/FormField.js @@ -6,10 +6,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -const FormField = ({ className, input, label, ...otherProps }) => ( +const FormField = ({ + className, + input: inputComponent, + label, + ...otherProps +}) => (
- {input || } + {inputComponent || }
); diff --git a/yarn.lock b/yarn.lock index b6dda5a6..4cb38944 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2468,14 +2468,10 @@ big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" -bignumber.js@4.1.0: +bignumber.js@4.1.0, bignumber.js@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" -bignumber.js@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" - binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" -- GitLab From ecab746a0eec7ca1c39ee6c65457dca480bd6deb Mon Sep 17 00:00:00 2001 From: Amaury Martiny Date: Thu, 21 Jun 2018 17:07:54 +0200 Subject: [PATCH 2/4] 2 lines for address (fix #71) --- packages/light-react/src/Send/TxForm/TxForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/light-react/src/Send/TxForm/TxForm.js b/packages/light-react/src/Send/TxForm/TxForm.js index a0a7a285..38ee6fbc 100644 --- a/packages/light-react/src/Send/TxForm/TxForm.js +++ b/packages/light-react/src/Send/TxForm/TxForm.js @@ -168,7 +168,7 @@ class Send extends Component { Date: Thu, 21 Jun 2018 17:36:15 +0200 Subject: [PATCH 3/4] Add message when wrong password --- .../light-react/src/Send/Signer/Signer.js | 51 ++++++++++++++----- .../light-react/src/Send/TxForm/TxForm.js | 10 ++-- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/packages/light-react/src/Send/Signer/Signer.js b/packages/light-react/src/Send/Signer/Signer.js index 91c9cfb7..5c0ecdb9 100644 --- a/packages/light-react/src/Send/Signer/Signer.js +++ b/packages/light-react/src/Send/Signer/Signer.js @@ -4,9 +4,11 @@ // SPDX-License-Identifier: MIT import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; import { FormField, Header } from 'light-ui'; import { inject, observer } from 'mobx-react'; import { Link } from 'react-router-dom'; +import ReactTooltip from 'react-tooltip'; import TokenBalance from '../../Tokens/TokensList/TokenBalance'; @@ -14,26 +16,31 @@ import TokenBalance from '../../Tokens/TokensList/TokenBalance'; @observer class Signer extends Component { state = { + error: null, isSending: false, password: '' }; - handleAccept = e => { + handleAccept = event => { const { history, sendStore } = this.props; const { password } = this.state; - e.preventDefault(); + event.preventDefault(); this.setState({ isSending: true }, () => { sendStore .acceptRequest(password) .then(() => history.push('/send/sent')) - .catch(() => this.setState({ isSending: false })); + .catch(error => { + this.setState({ error, isSending: false }, () => + ReactTooltip.show(findDOMNode(this.tooltip)) + ); + }); }); }; handleChangePassword = ({ target: { value } }) => { - this.setState({ password: value }); + this.setState({ error: null, password: value }); }; handleReject = () => { @@ -47,11 +54,15 @@ class Signer extends Component { }); }; + handleTooltipRef = ref => { + this.tooltip = ref; + }; + render () { const { sendStore: { token, tx, txStatus } } = this.props; - const { isSending, password } = this.state; + const { error, isSending, password } = this.state; return (
@@ -85,13 +96,18 @@ class Signer extends Component {

Enter your password to confirm this transaction.

- +
+ +