// COMPONENTS import Spinner from 'components/ui/Spinner'; import TabSection from 'containers/TabSection'; import { BalanceSidebar } from 'components'; import { UnlockHeader } from 'components/ui'; import { NonceField, AddressField, AmountField, ConfirmationModal, CustomMessage, DataField, GasField } from './components'; import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded'; // CONFIG import { donationAddressMap, NetworkConfig } from 'config/data'; // LIBS import { stripHexPrefix } from 'libs/values'; import { TransactionWithoutGas } from 'libs/messages'; import { RPCNode } from 'libs/nodes'; import { BroadcastTransactionStatus, CompleteTransaction, confirmAndSendWeb3Transaction, formatTxInput, generateCompleteTransaction, getBalanceMinusGasCosts, TransactionInput } from 'libs/transaction'; import { UnitKey, Wei, getDecimal, toWei } from 'libs/units'; import { isValidETHAddress } from 'libs/validators'; // LIBS import { IWallet, Balance, Web3Wallet, LedgerWallet, TrezorWallet } from 'libs/wallet'; import pickBy from 'lodash/pickBy'; import React from 'react'; // REDUX import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { showNotification, TShowNotification } from 'actions/notifications'; import { broadcastTx, TBroadcastTx, resetWallet, TResetWallet } from 'actions/wallet'; import { pollOfflineStatus as dPollOfflineStatus, TPollOfflineStatus } from 'actions/config'; // SELECTORS import { getGasPriceGwei, getNetworkConfig, getNodeLib } from 'selectors/config'; import { getTokenBalances, getTokens, getTxFromBroadcastTransactionStatus, MergedToken, TokenBalance } from 'selectors/wallet'; import translate from 'translations'; // UTILS import { formatGasLimit } from 'utils/formatters'; import { getParam } from 'utils/helpers'; import queryString from 'query-string'; // MISC import customMessages from './messages'; interface State { hasQueryString: boolean; readOnly: boolean; to: string; // amount value value: string; unit: UnitKey; token?: MergedToken | null; gasLimit: string; data: string; gasChanged: boolean; transaction: CompleteTransaction | null; showTxConfirm: boolean; generateDisabled: boolean; nonce: number | null | undefined; hasSetDefaultNonce: boolean; generateTxProcessing: boolean; walletAddress: string | null; } interface Props { wallet: IWallet; balance: Balance; nodeLib: RPCNode; network: NetworkConfig; tokens: MergedToken[]; tokenBalances: TokenBalance[]; gasPrice: Wei; transactions: BroadcastTransactionStatus[]; showNotification: TShowNotification; broadcastTx: TBroadcastTx; resetWallet: TResetWallet; offline: boolean; forceOffline: boolean; pollOfflineStatus: TPollOfflineStatus; location: { search: string }; } const initialState: State = { hasQueryString: false, readOnly: false, to: '', value: '', unit: 'ether', token: null, gasLimit: '21000', data: '', gasChanged: false, showTxConfirm: false, transaction: null, generateDisabled: true, nonce: null, hasSetDefaultNonce: false, generateTxProcessing: false, walletAddress: null }; export class SendTransaction extends React.Component { public state: State = initialState; public componentDidMount() { this.props.pollOfflineStatus(); const queryPresets = pickBy(this.parseQuery()); if (Object.keys(queryPresets).length) { this.setState(state => { return { ...state, ...queryPresets, hasQueryString: true }; }); } } public haveFieldsChanged(prevState) { return ( this.state.to !== prevState.to || this.state.value !== prevState.value || this.state.unit !== prevState.unit || this.state.data !== prevState.data ); } public shouldReEstimateGas(prevState) { // TODO listen to gas price changes here, debounce the call, and handle gas estimation return ( // if any relevant fields changed this.haveFieldsChanged(prevState) && // if gas has not changed !this.state.gasChanged && // if we have valid tx (this.isValid() || (this.props.offline || this.props.forceOffline)) ); } public handleGasEstimationOnUpdate(prevState) { if (this.shouldReEstimateGas(prevState)) { this.estimateGas(); } } public handleGenerateDisabledOnUpdate() { if (this.state.generateDisabled === this.isValid()) { this.setState({ generateDisabled: !this.isValid() }); } } public handleBroadcastTransactionOnUpdate() { // handle clearing the form once broadcast transaction promise resolves and compontent updates const componentStateTransaction = this.state.transaction; if (componentStateTransaction) { // lives in redux state const currentTxAsSignedTransaction = getTxFromBroadcastTransactionStatus( this.props.transactions, componentStateTransaction.signedTx ); // if there is a matching tx in redux state if (currentTxAsSignedTransaction) { // if the broad-casted transaction attempt is successful, clear the form if (currentTxAsSignedTransaction.successfullyBroadcast) { this.resetTx(); } } } } public async handleSetNonceWhenOfflineOnUpdate() { const { offline, forceOffline, wallet, nodeLib } = this.props; const { hasSetDefaultNonce, nonce } = this.state; const unlocked = !!wallet; if (unlocked) { const from = await wallet.getAddressString(); if (forceOffline && !offline && !hasSetDefaultNonce) { const nonceHex = await nodeLib.getTransactionCount(from); const newNonce = parseInt(stripHexPrefix(nonceHex), 10); this.setState({ nonce: newNonce, hasSetDefaultNonce: true }); } if (!forceOffline && !offline && nonce) { // set hasSetDefaultNonce back to false in case user toggles force offline several times this.setState({ nonce: null, hasSetDefaultNonce: false }); } } } public handleWalletStateOnUpdate(prevProps) { if (this.props.wallet !== prevProps.wallet && !!prevProps.wallet) { this.setState(initialState); } } public async setWalletAddressOnUpdate() { if (this.props.wallet) { const walletAddress = await this.props.wallet.getAddressString(); if (walletAddress !== this.state.walletAddress) { this.setState({ walletAddress }); } } } public componentDidUpdate(prevProps: Props, prevState: State) { this.handleGasEstimationOnUpdate(prevState); this.handleGenerateDisabledOnUpdate(); this.handleBroadcastTransactionOnUpdate(); this.handleSetNonceWhenOfflineOnUpdate(); this.handleWalletStateOnUpdate(prevProps); this.setWalletAddressOnUpdate(); } public onNonceChange = (value: number) => { this.setState({ nonce: value }); }; public render() { const unlocked = !!this.props.wallet; const { to, unit, gasLimit, data, readOnly, hasQueryString, showTxConfirm, transaction, nonce, generateTxProcessing } = this.state; const { offline, forceOffline, balance } = this.props; const customMessage = customMessages.find(m => m.to === to); const decimal = unit === 'ether' ? getDecimal('ether') : (this.state.token && this.state.token.decimal) || 0; const isWeb3Wallet = this.props.wallet instanceof Web3Wallet; const isLedgerWallet = this.props.wallet instanceof LedgerWallet; const isTrezorWallet = this.props.wallet instanceof TrezorWallet; return (
{translate('NAV_SendEther')} {offline || forceOffline ? (Offline) : null} } allowReadOnly={true} />
{/* Send Form */} {unlocked && !((offline || forceOffline) && isWeb3Wallet) && (
{hasQueryString && (

{translate('WARN_Send_Link')}

)} !token.balance.eqn(0)) .map(token => token.symbol) .sort()} onAmountChange={this.onAmountChange} isReadOnly={readOnly} onUnitChange={this.onUnitChange} /> {(offline || forceOffline) && (
)} {unit === 'ether' && ( )}
{generateTxProcessing && (
{isLedgerWallet || isTrezorWallet ? (

Confirm transaction on hardware wallet

) : ( )}
)} {transaction && (