// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import BigNumber from 'bignumber.js';
// import { isHexString } from 'ethereumjs-util';
import { chainId$, transactionCountOf$ } from '@parity/light.js';
import { Clickable, Form as FetherForm, Header } from 'fether-ui';
import createDecorator from 'final-form-calculate';
import debounce from 'debounce-promise';
import { Field, Form } from 'react-final-form';
import { fromWei, toWei } from '@parity/api/lib/util/wei';
import { inject, observer } from 'mobx-react';
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 i18n, { packageNS } from '../../i18n';
import Debug from '../../utils/debug';
import { estimateGas } from '../../utils/transaction';
import RequireHealthOverlay from '../../RequireHealthOverlay';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import TxDetails from './TxDetails';
import {
chainIdToString,
isEtcChainId,
isNotErc20TokenAddress
} from '../../utils/chain';
import withAccount from '../../utils/withAccount';
import withBalance, { withEthBalance } from '../../utils/withBalance';
import withTokens from '../../utils/withTokens';
const DEFAULT_AMOUNT_MAX_CHARS = 9;
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
// Reference: https://ethereum.stackexchange.com/questions/1106/is-there-a-limit-for-transaction-size
const ESTIMATED_MAX_TOTAL_FEE = 0.002; // In ETH
const debug = Debug('TxForm');
// Luke's custom `isHexString`.
// See https://github.com/MyCryptoHQ/MyCrypto/issues/2592#issuecomment-496432227
function isHexString (value, length) {
if (typeof value !== 'string') {
return false;
}
// Check for hex prefix
if (value.substring(0, 2) !== '0x') {
return false;
}
// Extract hex value without the 0x prefix
// TODO - possible replace with `stripHexPrefix(value).
// See https://github.com/ethjs/ethjs-util/blob/master/dist/ethjs-util.js#L2120
const postfixValue = value.slice(2);
if (!postfixValue.length) {
console.log('No characters found after 0x prefix');
return false;
}
// Check if non-hex character exists after 0x prefix
const match = /[^0-9A-Fa-f]/.exec(postfixValue);
if (match) {
console.log('Non-hex character match found at ' + match.index);
return false;
}
// Check if the hex string is the expected length
if (length && postfixValue.length !== length) {
console.log(
`Hex value length is ${postfixValue.length} but should be ${length}`
);
return false;
}
return true;
}
@inject('parityStore', 'sendStore')
@withTokens
@withProps(({ match: { params: { tokenAddress } }, tokens }) => ({
token: tokens[tokenAddress]
}))
@withAccount
@light({
// We need to wait for 3 values that might take time:
// - ethBalance: to check that we have enough balance to send amount+fees
// It may be ETH or ETC corresponding to whether we are connected to the
// 'foundation' or 'classic' chain.
// - 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 or ETC)
@withEthBalance // ETH or ETC balance
@observer
class TxForm extends Component {
state = {
maxSelected: false,
showDetails: false
};
decorator = createDecorator({
// FIXME - Why can't we estimate gas when we update the next line to
// include the `data` (i.e. `field: /data|to|amount/,`).
// If we do then it gives error in console:
// ```
// eth_estimateGas([{"data":"0x0000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaffffffffffff",
// ... "value":"0x0"}]): -32015: Transaction execution error.
// ```
//
// And in the UI's "Transaction Details" section it displays:
// ```
// Gas Limit: -1
// Fee: -0.000000004 ETH (gas limit * gas price)
// Total Amount: -4e-9 ETH
// ```
field: /to|amount/, // when the value of these fields change...
updates: {
// ...set field "gas"
gas: async (value, allValues) => {
const { parityStore, token } = this.props;
let newGasEstimate = null;
if (this.preValidate(allValues) === true) {
try {
newGasEstimate = await estimateGas(
allValues,
token,
parityStore.api
);
} catch (error) {
return new BigNumber(-1);
}
}
return newGasEstimate;
}
}
});
changeAmountFontSize = amount => {
const amountLen = amount.toString().length;
if (amountLen > MEDIUM_AMOUNT_MAX_CHARS) {
return '-resize-font-small'; // Resize to fit an amount as small as one Wei
} else if (
MEDIUM_AMOUNT_MAX_CHARS >= amountLen &&
amountLen > DEFAULT_AMOUNT_MAX_CHARS
) {
return '-resize-font-medium';
}
return '-resize-font-default';
};
calculateMax = (gas, gasPrice) => {
const { token, balance } = this.props;
const gasBn = gas ? new BigNumber(gas) : new BigNumber(21000);
const gasPriceBn = new BigNumber(gasPrice);
let output;
if (isNotErc20TokenAddress(token.address)) {
output = fromWei(
toWei(balance).minus(gasBn.multipliedBy(toWei(gasPriceBn, 'shannon')))
);
output = output.isNegative() ? new BigNumber(0) : output;
} else {
output = balance;
}
return output;
};
isEstimatedTxFee = values => {
if (
// Allow estimating tx fee when the amount is zero
// values.amount &&
values.gas &&
values.gasPrice &&
!isNaN(values.amount) &&
!values.gas.isNaN() &&
!isNaN(values.gasPrice)
) {
return true;
}
return false;
};
estimatedTxFee = values => {
if (!this.isEstimatedTxFee(values)) {
return null;
}
return values.gas.multipliedBy(toWei(values.gasPrice, 'shannon'));
};
handleSubmit = values => {
const {
account: { address, type },
history,
sendStore,
token
} = this.props;
sendStore.setTx({ ...values, token });
if (type === 'signer') {
history.push(`/send/${token.address}/from/${address}/txqrcode`);
} else {
history.push(`/send/${token.address}/from/${address}/unlock`);
}
};
recalculateMax = (args, state, { changeValue }) => {
changeValue(state, 'amount', value => {
return this.calculateMax(
state.formState.values.gas,
state.formState.values.gasPrice
);
});
};
toggleMax = () => {
this.setState({ maxSelected: !this.state.maxSelected });
};
showDetailsAnchor = () => {
return (