aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/send_
diff options
context:
space:
mode:
authorDan <danjm.com@gmail.com>2018-05-01 10:42:57 +0800
committerDan <danjm.com@gmail.com>2018-05-01 10:42:57 +0800
commit2f78fffbdbb0e41d73bcde2c15c88fff095614b7 (patch)
tree24d68a267304d085ee1b7c705ce5ade53c9439c3 /ui/app/components/send_
parentf96c13d616e429447ac0a6a24c6aeee902162b88 (diff)
parent954394f81090b1a6a4afe55243caa3671b88addc (diff)
downloadtangerine-wallet-browser-2f78fffbdbb0e41d73bcde2c15c88fff095614b7.tar.gz
tangerine-wallet-browser-2f78fffbdbb0e41d73bcde2c15c88fff095614b7.tar.zst
tangerine-wallet-browser-2f78fffbdbb0e41d73bcde2c15c88fff095614b7.zip
Merge branch 'i3725-refactor-send-component-' into i3725-refactor-send-component-2
Diffstat (limited to 'ui/app/components/send_')
-rw-r--r--ui/app/components/send_/account-list-item/account-list-item-README.md0
-rw-r--r--ui/app/components/send_/account-list-item/account-list-item.component.js74
-rw-r--r--ui/app/components/send_/account-list-item/account-list-item.container.js15
-rw-r--r--ui/app/components/send_/account-list-item/account-list-item.scss0
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js54
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js40
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js9
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js22
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js96
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js69
-rw-r--r--ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js9
-rw-r--r--ui/app/components/send_/send-content/send-content.component.js23
-rw-r--r--ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js75
-rw-r--r--ui/app/components/send_/send-content/send-from-row/send-from-row.component.js29
-rw-r--r--ui/app/components/send_/send-content/send-from-row/send-from-row.container.js34
-rw-r--r--ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js4
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js49
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js26
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js9
-rw-r--r--ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js10
-rw-r--r--ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js3
-rw-r--r--ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js22
-rw-r--r--ui/app/components/send_/send-content/send-to-row/send-to-row.component.js48
-rw-r--r--ui/app/components/send_/send-content/send-to-row/send-to-row.container.js28
-rw-r--r--ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js2
-rw-r--r--ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js12
-rw-r--r--ui/app/components/send_/send-footer/send-footer.component.js93
-rw-r--r--ui/app/components/send_/send-footer/send-footer.container.js106
-rw-r--r--ui/app/components/send_/send-footer/send-footer.selectors.js12
-rw-r--r--ui/app/components/send_/send-footer/send-footer.utils.js84
-rw-r--r--ui/app/components/send_/send-header/send-header.component.js17
-rw-r--r--ui/app/components/send_/send-header/send-header.container.js5
-rw-r--r--ui/app/components/send_/send.component.js142
-rw-r--r--ui/app/components/send_/send.constants.js44
-rw-r--r--ui/app/components/send_/send.container.js88
-rw-r--r--ui/app/components/send_/send.selectors.js292
-rw-r--r--ui/app/components/send_/send.utils.js188
37 files changed, 1582 insertions, 251 deletions
diff --git a/ui/app/components/send_/account-list-item/account-list-item-README.md b/ui/app/components/send_/account-list-item/account-list-item-README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/components/send_/account-list-item/account-list-item-README.md
diff --git a/ui/app/components/send_/account-list-item/account-list-item.component.js b/ui/app/components/send_/account-list-item/account-list-item.component.js
new file mode 100644
index 000000000..b8407d147
--- /dev/null
+++ b/ui/app/components/send_/account-list-item/account-list-item.component.js
@@ -0,0 +1,74 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { checksumAddress } from '../../../util'
+import Identicon from '../../identicon'
+import CurrencyDisplay from '../../send/currency-display'
+
+export default class AccountListItem extends Component {
+
+ static propTypes = {
+ account: PropTypes.object,
+ className: PropTypes.string,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ displayAddress: PropTypes.bool,
+ displayBalance: PropTypes.bool,
+ handleClick: PropTypes.func,
+ icon: PropTypes.node,
+ };
+
+ render () {
+ const {
+ account,
+ className,
+ conversionRate,
+ currentCurrency,
+ displayAddress = false,
+ displayBalance = true,
+ handleClick,
+ icon = null,
+ } = this.props
+
+ const { name, address, balance } = account || {}
+
+ return (<div
+ className={`account-list-item ${className}`}
+ onClick={() => handleClick({ name, address, balance })}
+ >
+
+ <div className="account-list-item__top-row">
+ <Identicon
+ address={address}
+ className="account-list-item__identicon"
+ diameter={18}
+ />
+
+ <div className="account-list-item__account-name">{ name || address }</div>
+
+ {icon && <div className="account-list-item__icon">{ icon }</div>}
+
+ </div>
+
+ {displayAddress && name && <div className="account-list-item__account-address">
+ { checksumAddress(address) }
+ </div>}
+
+ {displayBalance && <CurrencyDisplay
+ className="account-list-item__account-balances"
+ conversionRate={conversionRate}
+ convertedBalanceClassName="account-list-item__account-secondary-balance"
+ convertedCurrency={currentCurrency}
+ primaryBalanceClassName="account-list-item__account-primary-balance"
+ primaryCurrency="ETH"
+ readOnly={true}
+ value={balance}
+ />}
+
+ </div>)
+ }
+}
+
+AccountListItem.contextTypes = {
+ t: PropTypes.func,
+}
+
diff --git a/ui/app/components/send_/account-list-item/account-list-item.container.js b/ui/app/components/send_/account-list-item/account-list-item.container.js
new file mode 100644
index 000000000..3151b1f1d
--- /dev/null
+++ b/ui/app/components/send_/account-list-item/account-list-item.container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import {
+ getConversionRate,
+ getConvertedCurrency,
+} from '../send.selectors.js'
+import AccountListItem from './account-list-item.component'
+
+export default connect(mapStateToProps)(AccountListItem)
+
+function mapStateToProps (state) {
+ return {
+ conversionRate: getConversionRate(state),
+ currentCurrency: getConvertedCurrency(state),
+ }
+}
diff --git a/ui/app/components/send_/account-list-item/account-list-item.scss b/ui/app/components/send_/account-list-item/account-list-item.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/components/send_/account-list-item/account-list-item.scss
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
index e69de29bb..bdf12b738 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -0,0 +1,54 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class AmountMaxButton extends Component {
+
+ static propTypes = {
+ balance: PropTypes.string,
+ gasTotal: PropTypes.string,
+ maxModeOn: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ setAmountToMax: PropTypes.func,
+ setMaxModeTo: PropTypes.func,
+ tokenBalance: PropTypes.string,
+ };
+
+ setMaxAmount () {
+ const {
+ balance,
+ gasTotal,
+ selectedToken,
+ setAmountToMax,
+ tokenBalance,
+ } = this.props
+
+ setAmountToMax({
+ balance,
+ gasTotal,
+ selectedToken,
+ tokenBalance,
+ })
+ }
+
+ render () {
+ const { setMaxModeTo, maxModeOn } = this.props
+
+ return (
+ <div
+ className="send-v2__amount-max"
+ onClick={(event) => {
+ event.preventDefault()
+ setMaxModeTo(true)
+ this.setMaxAmount()
+ }}
+ >
+ {!maxModeOn ? this.context.t('max') : ''}
+ </div>
+ )
+ }
+
+}
+
+AmountMaxButton.contextTypes = {
+ t: PropTypes.func,
+}
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
index e69de29bb..a72f41775 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
+++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
@@ -0,0 +1,40 @@
+import { connect } from 'react-redux'
+import {
+ getGasTotal,
+ getSelectedToken,
+ getSendFromBalance,
+ getTokenBalance,
+} from '../../../send.selectors.js'
+import { getMaxModeOn } from './amount-max-button.selectors.js'
+import { calcMaxAmount } from './amount-max-button.utils.js'
+import {
+ updateSendAmount,
+ setMaxModeTo,
+} from '../../../../../actions'
+import AmountMaxButton from './amount-max-button.component'
+import {
+ updateSendErrors,
+} from '../../../../../ducks/send'
+
+export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton)
+
+function mapStateToProps (state) {
+
+ return {
+ balance: getSendFromBalance(state),
+ gasTotal: getGasTotal(state),
+ maxModeOn: getMaxModeOn(state),
+ selectedToken: getSelectedToken(state),
+ tokenBalance: getTokenBalance(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ setAmountToMax: maxAmountDataObject => {
+ dispatch(updateSendErrors({ amount: null }))
+ dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
+ },
+ setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
+ }
+}
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
index e69de29bb..69fec1994 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
+++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
@@ -0,0 +1,9 @@
+const selectors = {
+ getMaxModeOn,
+}
+
+module.exports = selectors
+
+function getMaxModeOn (state) {
+ return state.metamask.send.maxModeOn
+}
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
index e69de29bb..b490a7fd7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
+++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
@@ -0,0 +1,22 @@
+const {
+ multiplyCurrencies,
+ subtractCurrencies,
+} = require('../../../../../conversion-util')
+const ethUtil = require('ethereumjs-util')
+
+function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) {
+ const { decimals } = selectedToken || {}
+ const multiplier = Math.pow(10, Number(decimals || 0))
+
+ return selectedToken
+ ? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'})
+ : subtractCurrencies(
+ ethUtil.addHexPrefix(balance),
+ ethUtil.addHexPrefix(gasTotal),
+ { toNumericBase: 'hex' }
+ )
+}
+
+module.exports = {
+ calcMaxAmount,
+}
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
index e69de29bb..8e201ae41 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
+++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
@@ -0,0 +1,96 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component'
+import AmountMaxButton from './amount-max-button/amount-max-button.container'
+import CurrencyDisplay from '../../../send/currency-display'
+
+export default class SendAmountRow extends Component {
+
+ static propTypes = {
+ amount: PropTypes.string,
+ amountConversionRate: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ balance: PropTypes.string,
+ conversionRate: PropTypes.number,
+ convertedCurrency: PropTypes.string,
+ gasTotal: PropTypes.string,
+ inError: PropTypes.bool,
+ primaryCurrency: PropTypes.string,
+ selectedToken: PropTypes.object,
+ setMaxModeTo: PropTypes.func,
+ tokenBalance: PropTypes.string,
+ updateSendAmount: PropTypes.func,
+ updateSendAmountError: PropTypes.func,
+ }
+
+ validateAmount (amount) {
+ const {
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ updateSendAmountError,
+ } = this.props
+
+ updateSendAmountError({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ }
+
+ handleAmountChange (amount) {
+ const { updateSendAmount, setMaxModeTo } = this.props
+
+ setMaxModeTo(false)
+ this.validateAmount(amount)
+ updateSendAmount(amount)
+ }
+
+ render () {
+ const {
+ amount,
+ amountConversionRate,
+ convertedCurrency,
+ gasTotal,
+ inError,
+ primaryCurrency = 'ETH',
+ selectedToken,
+ } = this.props
+
+ return (
+ <SendRowWrapper
+ label={`${this.context.t('amount')}:`}
+ showError={inError}
+ errorType={'amount'}
+ >
+ {!inError && gasTotal && <AmountMaxButton />}
+ <CurrencyDisplay
+ conversionRate={amountConversionRate}
+ convertedCurrency={convertedCurrency}
+ handleChange={newAmount => this.handleAmountChange(newAmount)}
+ inError={inError}
+ primaryCurrency={primaryCurrency}
+ selectedToken={selectedToken}
+ value={amount || '0x0'}
+ />
+ </SendRowWrapper>
+ )
+ }
+
+}
+
+SendAmountRow.contextTypes = {
+ t: PropTypes.func,
+}
+
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
index 6ae80e7f2..13888ec53 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
+++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
@@ -1,48 +1,51 @@
+import { connect } from 'react-redux'
import {
- getSelectedToken,
- getPrimaryCurrency,
getAmountConversionRate,
+ getConversionRate,
getConvertedCurrency,
- getSendAmount,
getGasTotal,
- getSelectedBalance,
+ getPrimaryCurrency,
+ getSelectedToken,
+ getSendAmount,
+ getSendFromBalance,
getTokenBalance,
-} from '../../send.selectors.js'
+} from '../../send.selectors'
import {
- getMaxModeOn,
- getSendAmountError,
-} from './send-amount-row.selectors.js'
-import { getAmountErrorObject } from './send-to-row.utils.js'
+ sendAmountIsInError,
+} from './send-amount-row.selectors'
+import { getAmountErrorObject } from '../../send.utils'
import {
- updateSendErrors,
- updateSendTo,
-} from '../../../actions'
+ setMaxModeTo,
+ updateSendAmount,
+} from '../../../../actions'
import {
- openToDropdown,
- closeToDropdown,
-} from '../../../ducks/send'
-import SendToRow from './send-to-row.component'
+ updateSendErrors,
+} from '../../../../ducks/send'
+import SendAmountRow from './send-amount-row.component'
-export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
+export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow)
function mapStateToProps (state) {
-updateSendTo
-return {
- to: getSendTo(state),
- toAccounts: getSendToAccounts(state),
- toDropdownOpen: getToDropdownOpen(state),
- inError: sendToIsInError(state),
- network: getCurrentNetwork(state),
-}
+ return {
+ amount: getSendAmount(state),
+ amountConversionRate: getAmountConversionRate(state),
+ balance: getSendFromBalance(state),
+ conversionRate: getConversionRate(state),
+ convertedCurrency: getConvertedCurrency(state),
+ gasTotal: getGasTotal(state),
+ inError: sendAmountIsInError(state),
+ primaryCurrency: getPrimaryCurrency(state),
+ selectedToken: getSelectedToken(state),
+ tokenBalance: getTokenBalance(state),
+ }
}
function mapDispatchToProps (dispatch) {
-return {
- updateSendToError: (to) => {
- dispatch(updateSendErrors(getToErrorObject(to)))
- },
- updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
- openToDropdown: () => dispatch(()),
- closeToDropdown: () => dispatch(()),
+ return {
+ setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
+ updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
+ updateSendAmountError: (amountDataObject) => {
+ dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
+ },
+ }
}
-} \ No newline at end of file
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js
index e69de29bb..fb08c7ed7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js
+++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js
@@ -0,0 +1,9 @@
+const selectors = {
+ sendAmountIsInError,
+}
+
+module.exports = selectors
+
+function sendAmountIsInError (state) {
+ return Boolean(state.send.errors.amount)
+}
diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js
index e69de29bb..e213cfcf3 100644
--- a/ui/app/components/send_/send-content/send-content.component.js
+++ b/ui/app/components/send_/send-content/send-content.component.js
@@ -0,0 +1,23 @@
+import React, { Component } from 'react'
+import PageContainerContent from '../../page-container/page-container-content.component'
+import SendAmountRow from './send-amount-row/send-amount-row.container'
+import SendFromRow from './send-from-row/send-from-row.container'
+import SendGasRow from './send-gas-row/send-gas-row.container'
+import SendToRow from './send-to-row/send-to-row.container'
+
+export default class SendContent extends Component {
+
+ render () {
+ return (
+ <PageContainerContent>
+ <div className="send-v2__form">
+ <SendFromRow />
+ <SendToRow />
+ <SendAmountRow />
+ <SendGasRow />
+ </div>
+ </PageContainerContent>
+ )
+ }
+
+}
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js
index e69de29bb..337228122 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js
+++ b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js
@@ -0,0 +1,75 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import AccountListItem from '../../../account-list-item/account-list-item.container'
+
+export default class FromDropdown extends Component {
+
+ static propTypes = {
+ accounts: PropTypes.array,
+ closeDropdown: PropTypes.func,
+ dropdownOpen: PropTypes.bool,
+ onSelect: PropTypes.func,
+ openDropdown: PropTypes.func,
+ selectedAccount: PropTypes.object,
+ };
+
+ renderListItemIcon (icon, color) {
+ return <i className={`fa ${icon} fa-lg`} style={ { color } }/>
+ }
+
+ getListItemIcon (currentAccount, selectedAccount) {
+ return currentAccount.address === selectedAccount.address
+ ? this.renderListItemIcon('fa-check', '#02c9b1')
+ : null
+ }
+
+ renderDropdown () {
+ const {
+ accounts,
+ closeDropdown,
+ onSelect,
+ selectedAccount,
+ } = this.props
+
+ return (<div>
+ <div
+ className="send-v2__from-dropdown__close-area"
+ onClick={() => closeDropdown}
+ />
+ <div className="send-v2__from-dropdown__list">
+ {accounts.map((account, index) => <AccountListItem
+ account={account}
+ className="account-list-item__dropdown"
+ handleClick={() => {
+ onSelect(account)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account, selectedAccount.address)}
+ key={`from-dropdown-account-#${index}`}
+ />)}
+ </div>
+ </div>)
+ }
+
+ render () {
+ const {
+ dropdownOpen,
+ openDropdown,
+ selectedAccount,
+ } = this.props
+
+ return <div className="send-v2__from-dropdown">
+ <AccountListItem
+ account={selectedAccount}
+ handleClick={openDropdown}
+ icon={this.renderListItemIcon('fa-caret-down', '#dedede')}
+ />
+ {dropdownOpen && this.renderDropdown()},
+ </div>
+ }
+
+}
+
+FromDropdown.contextTypes = {
+ t: PropTypes.func,
+}
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js
index 7582cb2e6..17e7f8e46 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js
+++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js
@@ -1,60 +1,59 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import SendRowWrapper from '../../../send/from-dropdown'
-import FromDropdown from ''
+import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component'
+import FromDropdown from './from-dropdown/from-dropdown.component'
export default class SendFromRow extends Component {
static propTypes = {
closeFromDropdown: PropTypes.func,
- conversionRate: PropTypes.string,
- from: PropTypes.string,
+ conversionRate: PropTypes.number,
+ from: PropTypes.object,
fromAccounts: PropTypes.array,
fromDropdownOpen: PropTypes.bool,
openFromDropdown: PropTypes.func,
tokenContract: PropTypes.object,
updateSendFrom: PropTypes.func,
- updateSendTokenBalance: PropTypes.func,
+ setSendTokenBalance: PropTypes.func,
};
async handleFromChange (newFrom) {
const {
updateSendFrom,
tokenContract,
- updateSendTokenBalance,
+ setSendTokenBalance,
} = this.props
if (tokenContract) {
const usersToken = await tokenContract.balanceOf(newFrom.address)
- updateSendTokenBalance(usersToken)
+ setSendTokenBalance(usersToken)
}
updateSendFrom(newFrom)
}
render () {
const {
+ closeFromDropdown,
+ conversionRate,
from,
fromAccounts,
- conversionRate,
fromDropdownOpen,
- tokenContract,
openFromDropdown,
- closeFromDropdown,
} = this.props
return (
<SendRowWrapper label={`${this.context.t('from')}:`}>
<FromDropdown
- dropdownOpen={fromDropdownOpen}
accounts={fromAccounts}
- selectedAccount={from}
- onSelect={newFrom => this.handleFromChange(newFrom)}
- openDropdown={() => openFromDropdown()}
closeDropdown={() => closeFromDropdown()}
conversionRate={conversionRate}
+ dropdownOpen={fromDropdownOpen}
+ onSelect={newFrom => this.handleFromChange(newFrom)}
+ openDropdown={() => openFromDropdown()}
+ selectedAccount={from}
/>
</SendRowWrapper>
- );
+ )
}
}
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js
index 2ff3f0ccd..9e366445d 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js
+++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js
@@ -1,29 +1,31 @@
+import { connect } from 'react-redux'
import {
- getSendFrom,
- conversionRateSelector,
- getSelectedTokenContract,
- getCurrentAccountWithSendEtherInfo,
accountsWithSendEtherInfoSelector,
+ getConversionRate,
+ getSelectedTokenContract,
+ getSendFromObject,
} from '../../send.selectors.js'
-import { getFromDropdownOpen } from './send-from-row.selectors.js'
+import {
+ getFromDropdownOpen,
+} from './send-from-row.selectors.js'
import { calcTokenUpdateAmount } from './send-from-row.utils.js'
import {
- updateSendTokenBalance,
updateSendFrom,
-} from '../../../actions'
+ setSendTokenBalance,
+} from '../../../../actions'
import {
- openFromDropdown,
closeFromDropdown,
-} from '../../../ducks/send'
+ openFromDropdown,
+} from '../../../../ducks/send'
import SendFromRow from './send-from-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow)
function mapStateToProps (state) {
return {
- from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state),
+ conversionRate: getConversionRate(state),
+ from: getSendFromObject(state),
fromAccounts: accountsWithSendEtherInfoSelector(state),
- conversionRate: conversionRateSelector(state),
fromDropdownOpen: getFromDropdownOpen(state),
tokenContract: getSelectedTokenContract(state),
}
@@ -31,14 +33,14 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
- updateSendTokenBalance: (usersToken, selectedToken) => {
+ closeFromDropdown: () => dispatch(closeFromDropdown()),
+ openFromDropdown: () => dispatch(openFromDropdown()),
+ updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)),
+ setSendTokenBalance: (usersToken, selectedToken) => {
if (!usersToken) return
const tokenBalance = calcTokenUpdateAmount(selectedToken, selectedToken)
- dispatch(updateSendTokenBalance(tokenBalance))
+ dispatch(setSendTokenBalance(tokenBalance))
},
- updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)),
- openFromDropdown: () => dispatch(()),
- closeFromDropdown: () => dispatch(()),
}
}
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js
index 2be25816f..0aaaef793 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js
+++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js
@@ -1,6 +1,6 @@
const {
calcTokenAmount,
-} = require('../../token-util')
+} = require('../../../../token-util')
function calcTokenUpdateAmount (usersToken, selectedToken) {
const { decimals } = selectedToken || {}
@@ -8,5 +8,5 @@ function calcTokenUpdateAmount (usersToken, selectedToken) {
}
module.exports = {
- calcTokenUpdateAmount
+ calcTokenUpdateAmount,
}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
index e69de29bb..c62c110e0 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
+++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component'
+import GasFeeDisplay from '../../../send/gas-fee-display-v2'
+
+export default class SendGasRow extends Component {
+
+ static propTypes = {
+ closeFromDropdown: PropTypes.func,
+ conversionRate: PropTypes.number,
+ convertedCurrency: PropTypes.string,
+ from: PropTypes.string,
+ fromAccounts: PropTypes.array,
+ fromDropdownOpen: PropTypes.bool,
+ gasLoadingError: PropTypes.bool,
+ gasTotal: PropTypes.string,
+ openFromDropdown: PropTypes.func,
+ showCustomizeGasModal: PropTypes.func,
+ tokenContract: PropTypes.object,
+ updateSendFrom: PropTypes.func,
+ };
+
+ render () {
+ const {
+ conversionRate,
+ convertedCurrency,
+ gasLoadingError,
+ gasTotal,
+ showCustomizeGasModal,
+ } = this.props
+
+ return (
+ <SendRowWrapper label={`${this.context.t('gasFee')}:`}>
+ <GasFeeDisplay
+ conversionRate={conversionRate}
+ convertedCurrency={convertedCurrency}
+ gasLoadingError={gasLoadingError}
+ gasTotal={gasTotal}
+ onClick={() => showCustomizeGasModal()}
+ />
+ </SendRowWrapper>
+ )
+ }
+
+}
+
+SendGasRow.contextTypes = {
+ t: PropTypes.func,
+}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js
index e69de29bb..20d3daa59 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js
+++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import {
+ getConversionRate,
+ getConvertedCurrency,
+ getGasTotal,
+} from '../../send.selectors.js'
+import { sendGasIsInError } from './send-gas-row.selectors.js'
+import { showModal } from '../../../../actions'
+import SendGasRow from './send-gas-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
+
+function mapStateToProps (state) {
+ return {
+ conversionRate: getConversionRate(state),
+ convertedCurrency: getConvertedCurrency(state),
+ gasTotal: getGasTotal(state),
+ gasLoadingError: sendGasIsInError(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })),
+ }
+}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
index e69de29bb..ad4ef4877 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
+++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
@@ -0,0 +1,9 @@
+const selectors = {
+ sendGasIsInError,
+}
+
+module.exports = selectors
+
+function sendGasIsInError (state) {
+ return state.metamask.send.errors.gasLoading
+}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
index 08f830cc5..0d314208b 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
+++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
@@ -1,3 +1,6 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
export default class SendRowErrorMessage extends Component {
static propTypes = {
@@ -7,17 +10,18 @@ export default class SendRowErrorMessage extends Component {
render () {
const { errors, errorType } = this.props
+
const errorMessage = errors[errorType]
return (
errorMessage
- ? <div className='send-v2__error'>{errorMessage}</div>
+ ? <div className="send-v2__error">{this.context.t(errorMessage)}</div>
: null
- );
+ )
}
}
SendRowErrorMessage.contextTypes = {
t: PropTypes.func,
-} \ No newline at end of file
+}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
index 2278dbe63..59622047f 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
+++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
@@ -1,3 +1,4 @@
+import { connect } from 'react-redux'
import { getSendErrors } from '../../../send.selectors'
import SendRowErrorMessage from './send-row-error-message.component'
@@ -8,4 +9,4 @@ function mapStateToProps (state, ownProps) {
errors: getSendErrors(state),
errorType: ownProps.errorType,
}
-} \ No newline at end of file
+}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js
index a1ac591b9..707b3ae80 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js
+++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js
@@ -5,31 +5,35 @@ import SendRowErrorMessage from './send-row-error-message/send-row-error-message
export default class SendRowWrapper extends Component {
static propTypes = {
- label: PropTypes.string,
- showError: PropTypes.bool,
children: PropTypes.node,
errorType: PropTypes.string,
+ label: PropTypes.string,
+ showError: PropTypes.bool,
};
render () {
const {
- label,
- errorType = '',
- showError = false,
- children,
+ children,
+ errorType = '',
+ label,
+ showError = false,
} = this.props
+ const formField = Array.isArray(children) ? children[1] || children[0] : children
+ const customLabelContent = children.length > 1 ? children[0] : null
+
return (
<div className="send-v2__form-row">
<div className="send-v2__form-label">
{label}
- (showError && <SendRowErrorMessage errorType={errorType}/>)
+ {showError && <SendRowErrorMessage errorType={errorType}/>}
+ {customLabelContent}
</div>
<div className="send-v2__form-field">
- {children}
+ {formField}
</div>
</div>
- );
+ )
}
}
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
index abcb54efc..a6e4c1624 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
@@ -1,57 +1,59 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import SendRowWrapper from '../../../send/from-dropdown'
-import ToDropdown from '../../../ens-input'
+import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component'
+import EnsInput from '../../../ens-input'
export default class SendToRow extends Component {
static propTypes = {
+ closeToDropdown: PropTypes.func,
+ inError: PropTypes.bool,
+ network: PropTypes.string,
+ openToDropdown: PropTypes.func,
to: PropTypes.string,
toAccounts: PropTypes.array,
toDropdownOpen: PropTypes.bool,
- inError: PropTypes.bool,
updateSendTo: PropTypes.func,
updateSendToError: PropTypes.func,
- openToDropdown: PropTypes.func,
- closeToDropdown: PropTypes.func,
- network: PropTypes.number,
};
handleToChange (to, nickname = '') {
const { updateSendTo, updateSendToError } = this.props
updateSendTo(to, nickname)
- updateSendErrors(to)
+ updateSendToError(to)
}
render () {
const {
- from,
- fromAccounts,
- conversionRate,
- fromDropdownOpen,
- tokenContract,
- openToDropdown,
closeToDropdown,
- network,
inError,
+ network,
+ openToDropdown,
+ to,
+ toAccounts,
+ toDropdownOpen,
} = this.props
return (
- <SendRowWrapper label={`${this.context.t('to')}:`}>
+ <SendRowWrapper
+ errorType={'to'}
+ label={`${this.context.t('to')}:`}
+ showError={inError}
+ >
<EnsInput
- name={'address'}
- placeholder={this.context.t('recipient Address')}
- network={network},
- to={to},
accounts={toAccounts}
- dropdownOpen={toDropdownOpen}
- openDropdown={() => openToDropdown()}
closeDropdown={() => closeToDropdown()}
- onChange={this.handleToChange}
+ dropdownOpen={toDropdownOpen}
inError={inError}
+ name={'address'}
+ network={network}
+ onChange={(newTo, newNickname) => this.handleToChange(newTo, newNickname)}
+ openDropdown={() => openToDropdown()}
+ placeholder={this.context.t('recipientAddress')}
+ to={to}
/>
</SendRowWrapper>
- );
+ )
}
}
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js
index 1c446c168..bffdda49c 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js
+++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js
@@ -1,7 +1,8 @@
+import { connect } from 'react-redux'
import {
- getSendTo,
- getToAccounts,
getCurrentNetwork,
+ getSendTo,
+ getSendToAccounts,
} from '../../send.selectors.js'
import {
getToDropdownOpen,
@@ -9,35 +10,34 @@ import {
} from './send-to-row.selectors.js'
import { getToErrorObject } from './send-to-row.utils.js'
import {
- updateSendErrors,
updateSendTo,
-} from '../../../actions'
+} from '../../../../actions'
import {
- openToDropdown,
- closeToDropdown,
-} from '../../../ducks/send'
+ updateSendErrors,
+ openToDropdown,
+ closeToDropdown,
+} from '../../../../ducks/send'
import SendToRow from './send-to-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
function mapStateToProps (state) {
- updateSendTo
return {
+ inError: sendToIsInError(state),
+ network: getCurrentNetwork(state),
to: getSendTo(state),
toAccounts: getSendToAccounts(state),
toDropdownOpen: getToDropdownOpen(state),
- inError: sendToIsInError(state),
- network: getCurrentNetwork(state),
}
}
function mapDispatchToProps (dispatch) {
return {
+ closeToDropdown: () => dispatch(closeToDropdown()),
+ openToDropdown: () => dispatch(openToDropdown()),
+ updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
updateSendToError: (to) => {
dispatch(updateSendErrors(getToErrorObject(to)))
},
- updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
- openToDropdown: () => dispatch(()),
- closeToDropdown: () => dispatch(()),
}
-} \ No newline at end of file
+}
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js
index 05bb65fa3..8919014be 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js
+++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js
@@ -10,5 +10,5 @@ function getToDropdownOpen (state) {
}
function sendToIsInError (state) {
- return Boolean(state.metamask.send.to)
+ return Boolean(state.send.errors.to)
}
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
index 52bfde009..22e2e1f34 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
+++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
@@ -1,17 +1,21 @@
+const {
+ REQUIRED_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+} = require('../../send.constants')
const { isValidAddress } = require('../../../../util')
function getToErrorObject (to) {
let toError = null
if (!to) {
- toError = 'required'
+ toError = REQUIRED_ERROR
} else if (!isValidAddress(to)) {
- toError = 'invalidAddressRecipient'
+ toError = INVALID_RECIPIENT_ADDRESS_ERROR
}
-
+
return { to: toError }
}
module.exports = {
- getToErrorObject
+ getToErrorObject,
}
diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js
index e69de29bb..fc7a78a94 100644
--- a/ui/app/components/send_/send-footer/send-footer.component.js
+++ b/ui/app/components/send_/send-footer/send-footer.component.js
@@ -0,0 +1,93 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../page-container/page-container-footer'
+import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes'
+
+export default class SendFooter extends Component {
+
+ static propTypes = {
+ addToAddressBookIfNew: PropTypes.func,
+ amount: PropTypes.string,
+ clearSend: PropTypes.func,
+ disabled: PropTypes.bool,
+ editingTransactionId: PropTypes.string,
+ errors: PropTypes.object,
+ from: PropTypes.object,
+ gasLimit: PropTypes.string,
+ gasPrice: PropTypes.string,
+ gasTotal: PropTypes.string,
+ history: PropTypes.object,
+ selectedToken: PropTypes.object,
+ sign: PropTypes.func,
+ to: PropTypes.string,
+ toAccounts: PropTypes.array,
+ tokenBalance: PropTypes.string,
+ unapprovedTxs: PropTypes.object,
+ update: PropTypes.func,
+ };
+
+ onSubmit (event) {
+ event.preventDefault()
+ const {
+ addToAddressBookIfNew,
+ amount,
+ editingTransactionId,
+ from: {address: from},
+ gasLimit: gas,
+ gasPrice,
+ selectedToken,
+ sign,
+ to,
+ unapprovedTxs,
+ // updateTx,
+ update,
+ toAccounts,
+ } = this.props
+
+ // Should not be needed because submit should be disabled if there are no errors.
+ // const noErrors = !amountError && toError === null
+
+ // if (!noErrors) {
+ // return
+ // }
+
+ // TODO: add nickname functionality
+ addToAddressBookIfNew(to, toAccounts)
+
+ editingTransactionId
+ ? update({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ })
+ : sign({ selectedToken, to, amount, from, gas, gasPrice })
+
+ this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+
+
+ render () {
+ const { clearSend, disabled, history } = this.props
+
+ return (
+ <PageContainerFooter
+ onCancel={() => {
+ clearSend()
+ history.push(DEFAULT_ROUTE)
+ }}
+ onSubmit={e => this.onSubmit(e)}
+ disabled={disabled}
+ />
+ )
+ }
+
+}
+
+SendFooter.contextTypes = {
+ t: PropTypes.func,
+}
diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send_/send-footer/send-footer.container.js
index e69de29bb..022b2db08 100644
--- a/ui/app/components/send_/send-footer/send-footer.container.js
+++ b/ui/app/components/send_/send-footer/send-footer.container.js
@@ -0,0 +1,106 @@
+import { connect } from 'react-redux'
+import ethUtil from 'ethereumjs-util'
+import {
+ addToAddressBook,
+ clearSend,
+ signTokenTx,
+ signTx,
+ updateTransaction,
+} from '../../../actions'
+import SendFooter from './send-footer.component'
+import {
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getSelectedToken,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendFromObject,
+ getSendTo,
+ getSendToAccounts,
+ getTokenBalance,
+ getUnapprovedTxs,
+} from '../send.selectors'
+import {
+ isSendFormInError,
+} from './send-footer.selectors'
+import {
+ addressIsNew,
+ constructTxParams,
+ constructUpdatedTx,
+ formShouldBeDisabled,
+} from './send-footer.utils'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
+
+function mapStateToProps (state) {
+ return {
+ amount: getSendAmount(state),
+ disabled: formShouldBeDisabled({
+ inError: isSendFormInError(state),
+ selectedToken: getSelectedToken(state),
+ tokenBalance: getTokenBalance(state),
+ gasTotal: getGasTotal(state),
+ }),
+ editingTransactionId: getSendEditingTransactionId(state),
+ from: getSendFromObject(state),
+ gasLimit: getGasLimit(state),
+ gasPrice: getGasPrice(state),
+ inError: isSendFormInError(state),
+ isToken: Boolean(getSelectedToken(state)),
+ selectedToken: getSelectedToken(state),
+ to: getSendTo(state),
+ toAccounts: getSendToAccounts(state),
+ unapprovedTxs: getUnapprovedTxs(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ clearSend: () => dispatch(clearSend()),
+ sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => {
+ const txParams = constructTxParams({
+ amount,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ })
+
+ selectedToken
+ ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams))
+ : dispatch(signTx(txParams))
+ },
+ update: ({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ }) => {
+ const editingTx = constructUpdatedTx({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+ })
+
+ dispatch(updateTransaction(editingTx))
+ },
+ addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
+ const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)
+ if (addressIsNew(toAccounts)) {
+ // TODO: nickname, i.e. addToAddressBook(recipient, nickname)
+ dispatch(addToAddressBook(hexPrefixedAddress, nickname))
+ }
+ },
+ }
+}
diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send_/send-footer/send-footer.selectors.js
index e69de29bb..e8fef6be6 100644
--- a/ui/app/components/send_/send-footer/send-footer.selectors.js
+++ b/ui/app/components/send_/send-footer/send-footer.selectors.js
@@ -0,0 +1,12 @@
+import { getSendErrors } from '../send.selectors'
+
+const selectors = {
+ isSendFormInError,
+}
+
+module.exports = selectors
+
+function isSendFormInError (state) {
+ const { amount, to } = getSendErrors(state)
+ return Boolean(amount || to !== null)
+}
diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send_/send-footer/send-footer.utils.js
index e69de29bb..353c0e347 100644
--- a/ui/app/components/send_/send-footer/send-footer.utils.js
+++ b/ui/app/components/send_/send-footer/send-footer.utils.js
@@ -0,0 +1,84 @@
+import ethAbi from 'ethereumjs-abi'
+import ethUtil from 'ethereumjs-util'
+import { TOKEN_TRANSFER_FUNCTION_SIGNATURE } from '../send.constants'
+
+function formShouldBeDisabled ({ inError, selectedToken, tokenBalance, gasTotal }) {
+ const missingTokenBalance = selectedToken && !tokenBalance
+ return inError || !gasTotal || missingTokenBalance
+}
+
+function addHexPrefixToObjectValues (obj) {
+ return Object.keys(obj).reduce((newObj, key) => {
+ return { ...newObj, [key]: ethUtil.addHexPrefix(obj[key]) }
+ }, {})
+}
+
+function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) {
+ const txParams = {
+ from,
+ value: '0',
+ gas,
+ gasPrice,
+ }
+
+ if (!selectedToken) {
+ txParams.value = amount
+ txParams.to = to
+ }
+
+ const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams)
+
+ return hexPrefixedTxParams
+}
+
+function constructUpdatedTx ({
+ amount,
+ editingTransactionId,
+ from,
+ gas,
+ gasPrice,
+ selectedToken,
+ to,
+ unapprovedTxs,
+}) {
+ const editingTx = {
+ ...unapprovedTxs[editingTransactionId],
+ txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }),
+ }
+
+ if (selectedToken) {
+ const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
+ ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+
+ Object.assign(editingTx.txParams, addHexPrefixToObjectValues({
+ value: '0',
+ to: selectedToken.address,
+ data,
+ }))
+ } else {
+ const { data } = unapprovedTxs[editingTransactionId].txParams
+
+ Object.assign(editingTx.txParams, addHexPrefixToObjectValues({
+ value: amount,
+ to,
+ data,
+ }))
+
+ if (typeof editingTx.txParams.data === 'undefined') {
+ delete editingTx.txParams.data
+ }
+ }
+}
+
+function addressIsNew (toAccounts, newAddress) {
+ return !toAccounts.find(({ address }) => newAddress === address)
+}
+
+module.exports = {
+ addressIsNew,
+ formShouldBeDisabled,
+ constructTxParams,
+ constructUpdatedTx,
+}
diff --git a/ui/app/components/send_/send-header/send-header.component.js b/ui/app/components/send_/send-header/send-header.component.js
index 99adfc7e8..dc4190b93 100644
--- a/ui/app/components/send_/send-header/send-header.component.js
+++ b/ui/app/components/send_/send-header/send-header.component.js
@@ -1,28 +1,29 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import PageContainerHeader from '../../page-container/page-container-header.component'
+import PageContainerHeader from '../../page-container/page-container-header'
+import { DEFAULT_ROUTE } from '../../../routes'
export default class SendHeader extends Component {
static propTypes = {
- isToken: PropTypes.bool,
clearSend: PropTypes.func,
- goHome: PropTypes.func,
+ history: PropTypes.object,
+ isToken: PropTypes.bool,
};
render () {
- const { isToken, clearSend, goHome } = this.props
+ const { isToken, clearSend, history } = this.props
return (
<PageContainerHeader
- title={isToken ? this.context.t('sendTokens') : this.context.t('sendETH')}
- subtitle={this.context.t('onlySendToEtherAddress')}
onClose={() => {
clearSend()
- goHome()
+ history.push(DEFAULT_ROUTE)
}}
+ subtitle={this.context.t('onlySendToEtherAddress')}
+ title={isToken ? this.context.t('sendTokens') : this.context.t('sendETH')}
/>
- );
+ )
}
}
diff --git a/ui/app/components/send_/send-header/send-header.container.js b/ui/app/components/send_/send-header/send-header.container.js
index a4d3ac54f..0c92da3a6 100644
--- a/ui/app/components/send_/send-header/send-header.container.js
+++ b/ui/app/components/send_/send-header/send-header.container.js
@@ -1,5 +1,5 @@
import { connect } from 'react-redux'
-import { goHome, clearSend } from '../../../actions'
+import { clearSend } from '../../../actions'
import SendHeader from './send-header.component'
import { getSelectedToken } from '../../../selectors'
@@ -7,13 +7,12 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendHeader)
function mapStateToProps (state) {
return {
- isToken: Boolean(getSelectedToken(state))
+ isToken: Boolean(getSelectedToken(state)),
}
}
function mapDispatchToProps (dispatch) {
return {
- goHome: () => dispatch(goHome()),
clearSend: () => dispatch(clearSend()),
}
}
diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js
index e69de29bb..e14a97537 100644
--- a/ui/app/components/send_/send.component.js
+++ b/ui/app/components/send_/send.component.js
@@ -0,0 +1,142 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import PersistentForm from '../../../lib/persistent-form'
+import {
+ getAmountErrorObject,
+ doesAmountErrorRequireUpdate,
+} from './send.utils'
+
+import SendHeader from './send-header/send-header.container'
+import SendContent from './send-content/send-content.component'
+import SendFooter from './send-footer/send-footer.container'
+
+export default class SendTransactionScreen extends PersistentForm {
+
+ static propTypes = {
+ amount: PropTypes.string,
+ amountConversionRate: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ ]),
+ conversionRate: PropTypes.number,
+ data: PropTypes.string,
+ editingTransactionId: PropTypes.string,
+ from: PropTypes.object,
+ gasLimit: PropTypes.string,
+ gasPrice: PropTypes.string,
+ gasTotal: PropTypes.string,
+ history: PropTypes.object,
+ network: PropTypes.string,
+ primaryCurrency: PropTypes.string,
+ selectedAddress: PropTypes.string,
+ selectedToken: PropTypes.object,
+ tokenBalance: PropTypes.string,
+ tokenContract: PropTypes.object,
+ updateAndSetGasTotal: PropTypes.func,
+ updateSendErrors: PropTypes.func,
+ updateSendTokenBalance: PropTypes.func,
+ };
+
+ updateGas () {
+ const {
+ data,
+ editingTransactionId,
+ gasLimit,
+ gasPrice,
+ selectedAddress,
+ selectedToken = {},
+ updateAndSetGasTotal,
+ } = this.props
+
+ updateAndSetGasTotal({
+ data,
+ editingTransactionId,
+ gasLimit,
+ gasPrice,
+ selectedAddress,
+ selectedToken,
+ })
+ }
+
+ componentDidUpdate (prevProps) {
+ const {
+ amount,
+ amountConversionRate,
+ conversionRate,
+ from: { address, balance },
+ gasTotal,
+ network,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ updateSendErrors,
+ updateSendTokenBalance,
+ tokenContract,
+ } = this.props
+
+ const {
+ from: { balance: prevBalance },
+ gasTotal: prevGasTotal,
+ tokenBalance: prevTokenBalance,
+ network: prevNetwork,
+ } = prevProps
+
+ const uninitialized = [prevBalance, prevGasTotal].every(n => n === null)
+
+ if (!uninitialized) {
+ const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({
+ balance,
+ gasTotal,
+ prevBalance,
+ prevGasTotal,
+ prevTokenBalance,
+ selectedToken,
+ tokenBalance,
+ })
+
+ if (amountErrorRequiresUpdate) {
+ const amountErrorObject = getAmountErrorObject({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ updateSendErrors(amountErrorObject)
+ }
+
+ if (network !== prevNetwork && network !== 'loading') {
+ updateSendTokenBalance({
+ selectedToken,
+ tokenContract,
+ address,
+ })
+ this.updateGas()
+ }
+ }
+ }
+
+ componentWillMount () {
+ this.updateGas()
+ }
+
+ render () {
+ const { history } = this.props
+
+ return (
+ <div className="page-container">
+ <SendHeader history={history}/>
+ <SendContent/>
+ <SendFooter history={history}/>
+ </div>
+ )
+ }
+
+}
+
+SendTransactionScreen.contextTypes = {
+ t: PropTypes.func,
+}
diff --git a/ui/app/components/send_/send.constants.js b/ui/app/components/send_/send.constants.js
new file mode 100644
index 000000000..d047ed704
--- /dev/null
+++ b/ui/app/components/send_/send.constants.js
@@ -0,0 +1,44 @@
+const ethUtil = require('ethereumjs-util')
+const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
+
+const MIN_GAS_PRICE_HEX = (100000000).toString(16)
+const MIN_GAS_PRICE_DEC = '100000000'
+const MIN_GAS_LIMIT_DEC = '21000'
+const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)
+
+const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX, {
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ fromNumericBase: 'hex',
+ toNumericBase: 'hex',
+ numberOfDecimals: 1,
+}))
+
+const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+})
+
+const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb'
+
+const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds'
+const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens'
+const NEGATIVE_ETH_ERROR = 'negativeETH'
+const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient'
+const REQUIRED_ERROR = 'required'
+
+module.exports = {
+ INSUFFICIENT_FUNDS_ERROR,
+ INSUFFICIENT_TOKENS_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+ MIN_GAS_LIMIT_DEC,
+ MIN_GAS_LIMIT_HEX,
+ MIN_GAS_PRICE_DEC,
+ MIN_GAS_PRICE_GWEI,
+ MIN_GAS_PRICE_HEX,
+ MIN_GAS_TOTAL,
+ NEGATIVE_ETH_ERROR,
+ REQUIRED_ERROR,
+ TOKEN_TRANSFER_FUNCTION_SIGNATURE,
+}
diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js
index e69de29bb..df605469a 100644
--- a/ui/app/components/send_/send.container.js
+++ b/ui/app/components/send_/send.container.js
@@ -0,0 +1,88 @@
+import { connect } from 'react-redux'
+import abi from 'ethereumjs-abi'
+import SendEther from './send.component'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import {
+ getAmountConversionRate,
+ getConversionRate,
+ getCurrentNetwork,
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getPrimaryCurrency,
+ getSelectedAddress,
+ getSelectedToken,
+ getSelectedTokenContract,
+ getSelectedTokenToFiatRate,
+ getSendAmount,
+ getSendEditingTransactionId,
+ getSendFromObject,
+ getTokenBalance,
+} from './send.selectors'
+import {
+ updateSendTokenBalance,
+ updateGasTotal,
+ setGasTotal,
+} from '../../actions'
+import {
+ updateSendErrors,
+} from '../../ducks/send'
+import {
+ calcGasTotal,
+ generateTokenTransferData,
+} from './send.utils.js'
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SendEther)
+
+function mapStateToProps (state) {
+ const selectedAddress = getSelectedAddress(state)
+ const selectedToken = getSelectedToken(state)
+
+ return {
+ amount: getSendAmount(state),
+ amountConversionRate: getAmountConversionRate(state),
+ conversionRate: getConversionRate(state),
+ data: generateTokenTransferData(abi, selectedAddress, selectedToken),
+ editingTransactionId: getSendEditingTransactionId(state),
+ from: getSendFromObject(state),
+ gasLimit: getGasLimit(state),
+ gasPrice: getGasPrice(state),
+ gasTotal: getGasTotal(state),
+ network: getCurrentNetwork(state),
+ primaryCurrency: getPrimaryCurrency(state),
+ selectedAddress: getSelectedAddress(state),
+ selectedToken: getSelectedToken(state),
+ tokenBalance: getTokenBalance(state),
+ tokenContract: getSelectedTokenContract(state),
+ tokenToFiatRate: getSelectedTokenToFiatRate(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ updateAndSetGasTotal: ({
+ data,
+ editingTransactionId,
+ gasLimit,
+ gasPrice,
+ selectedAddress,
+ selectedToken,
+ }) => {
+ !editingTransactionId
+ ? dispatch(updateGasTotal({ selectedAddress, selectedToken, data }))
+ : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
+ },
+ updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {
+ dispatch(updateSendTokenBalance({
+ selectedToken,
+ tokenContract,
+ address,
+ }))
+ },
+ updateSendErrors: newError => dispatch(updateSendErrors(newError)),
+ }
+}
diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js
index 8c088098e..761b15182 100644
--- a/ui/app/components/send_/send.selectors.js
+++ b/ui/app/components/send_/send.selectors.js
@@ -2,105 +2,95 @@ import { valuesFor } from '../../util'
import abi from 'human-standard-token-abi'
import {
multiplyCurrencies,
-} from './conversion-util'
+} from '../../conversion-util'
const selectors = {
+ accountsWithSendEtherInfoSelector,
+ autoAddToBetaUI,
+ getAddressBook,
+ getAmountConversionRate,
+ getConversionRate,
+ getConvertedCurrency,
+ getCurrentAccountWithSendEtherInfo,
+ getCurrentCurrency,
+ getCurrentNetwork,
+ getCurrentViewContext,
+ getForceGasMin,
+ getGasLimit,
+ getGasPrice,
+ getGasTotal,
+ getPrimaryCurrency,
+ getSelectedAccount,
getSelectedAddress,
getSelectedIdentity,
- getSelectedAccount,
getSelectedToken,
+ getSelectedTokenContract,
getSelectedTokenExchangeRate,
- getTokenExchangeRate,
- conversionRateSelector,
- transactionsSelector,
- accountsWithSendEtherInfoSelector,
- getCurrentAccountWithSendEtherInfo,
- getGasPrice,
- getGasLimit,
- getForceGasMin,
- getAddressBook,
- getSendFrom,
- getCurrentCurrency,
- getSendAmount,
getSelectedTokenToFiatRate,
- getSelectedTokenContract,
- autoAddToBetaUI,
- getSendMaxModeState,
- getCurrentViewContext,
+ getSendAmount,
+ getSendEditingTransactionId,
getSendErrors,
+ getSendFrom,
+ getSendFromBalance,
+ getSendFromObject,
+ getSendMaxModeState,
getSendTo,
- getCurrentNetwork,
+ getSendToAccounts,
+ getTokenBalance,
+ getTokenExchangeRate,
+ getUnapprovedTxs,
+ isSendFormInError,
+ transactionsSelector,
}
module.exports = selectors
-function getSelectedAddress (state) {
- const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
-
- return selectedAddress
-}
+function accountsWithSendEtherInfoSelector (state) {
+ const {
+ accounts,
+ identities,
+ } = state.metamask
-function getSelectedIdentity (state) {
- const selectedAddress = getSelectedAddress(state)
- const identities = state.metamask.identities
+ const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
+ return Object.assign({}, account, identities[key])
+ })
- return identities[selectedAddress]
+ return accountsWithSendEtherInfo
}
-function getSelectedAccount (state) {
- const accounts = state.metamask.accounts
- const selectedAddress = getSelectedAddress(state)
+function autoAddToBetaUI (state) {
+ const autoAddTransactionThreshold = 12
+ const autoAddAccountsThreshold = 2
+ const autoAddTokensThreshold = 1
- return accounts[selectedAddress]
-}
+ const numberOfTransactions = state.metamask.selectedAddressTxList.length
+ const numberOfAccounts = Object.keys(state.metamask.accounts).length
+ const numberOfTokensAdded = state.metamask.tokens.length
-function getSelectedToken (state) {
- const tokens = state.metamask.tokens || []
- const selectedTokenAddress = state.metamask.selectedTokenAddress
- const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
- const sendToken = state.metamask.send.token
+ const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&
+ (numberOfAccounts > autoAddAccountsThreshold) &&
+ (numberOfTokensAdded > autoAddTokensThreshold)
+ const userIsNotInBeta = !state.metamask.featureFlags.betaUI
- return selectedToken || sendToken || null
+ return userIsNotInBeta && userPassesThreshold
}
-function getSelectedTokenExchangeRate (state) {
- const tokenExchangeRates = state.metamask.tokenExchangeRates
- const selectedToken = getSelectedToken(state) || {}
- const { symbol = '' } = selectedToken
-
- const pair = `${symbol.toLowerCase()}_eth`
- const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
-
- return tokenExchangeRate
+function getAddressBook (state) {
+ return state.metamask.addressBook
}
-function getTokenExchangeRate (state, tokenSymbol) {
- const pair = `${tokenSymbol.toLowerCase()}_eth`
- const tokenExchangeRates = state.metamask.tokenExchangeRates
- const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
-
- return tokenExchangeRate
+function getAmountConversionRate (state) {
+ return getSelectedToken(state)
+ ? getSelectedTokenToFiatRate(state)
+ : getConversionRate(state)
}
-function conversionRateSelector (state) {
+function getConversionRate (state) {
return state.metamask.conversionRate
}
-function getAddressBook (state) {
- return state.metamask.addressBook
-}
-
-function accountsWithSendEtherInfoSelector (state) {
- const {
- accounts,
- identities,
- } = state.metamask
-
- const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
- return Object.assign({}, account, identities[key])
- })
-
- return accountsWithSendEtherInfo
+function getConvertedCurrency (state) {
+ return state.metamask.currentCurrency
}
function getCurrentAccountWithSendEtherInfo (state) {
@@ -110,20 +100,21 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress)
}
-function transactionsSelector (state) {
- const { network, selectedTokenAddress } = state.metamask
- const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
- const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
- const transactions = state.metamask.selectedAddressTxList || []
- const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
+function getCurrentCurrency (state) {
+ return state.metamask.currentCurrency
+}
- // console.log({txsToRender, selectedTokenAddress})
- return selectedTokenAddress
- ? txsToRender
- .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
- .sort((a, b) => b.time - a.time)
- : txsToRender
- .sort((a, b) => b.time - a.time)
+function getCurrentNetwork (state) {
+ return state.metamask.network
+}
+
+function getCurrentViewContext (state) {
+ const { currentView = {} } = state.appState
+ return currentView.context
+}
+
+function getForceGasMin (state) {
+ return state.metamask.send.forceGasMin
}
function getGasPrice (state) {
@@ -134,29 +125,64 @@ function getGasLimit (state) {
return state.metamask.send.gasLimit
}
-function getForceGasMin (state) {
- return state.metamask.send.forceGasMin
+function getGasTotal (state) {
+ return state.metamask.send.gasTotal
}
-function getSendFrom (state) {
- return state.metamask.send.from
+function getPrimaryCurrency (state) {
+ const selectedToken = getSelectedToken(state)
+ return selectedToken && selectedToken.symbol
}
-function getSendAmount (state) {
- return state.metamask.send.amount
+function getSelectedAccount (state) {
+ const accounts = state.metamask.accounts
+ const selectedAddress = getSelectedAddress(state)
+
+ return accounts[selectedAddress]
}
-function getSendMaxModeState (state) {
- return state.metamask.send.maxModeOn
+function getSelectedAddress (state) {
+ const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
+
+ return selectedAddress
}
-function getCurrentCurrency (state) {
- return state.metamask.currentCurrency
+function getSelectedIdentity (state) {
+ const selectedAddress = getSelectedAddress(state)
+ const identities = state.metamask.identities
+
+ return identities[selectedAddress]
+}
+
+function getSelectedToken (state) {
+ const tokens = state.metamask.tokens || []
+ const selectedTokenAddress = state.metamask.selectedTokenAddress
+ const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
+ const sendToken = state.metamask.send.token
+
+ return selectedToken || sendToken || null
+}
+
+function getSelectedTokenContract (state) {
+ const selectedToken = getSelectedToken(state)
+ return selectedToken
+ ? global.eth.contract(abi).at(selectedToken.address)
+ : null
+}
+
+function getSelectedTokenExchangeRate (state) {
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const selectedToken = getSelectedToken(state) || {}
+ const { symbol = '' } = selectedToken
+ const pair = `${symbol.toLowerCase()}_eth`
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates && tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
}
function getSelectedTokenToFiatRate (state) {
const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
- const conversionRate = conversionRateSelector(state)
+ const conversionRate = getConversionRate(state)
const tokenToFiatRate = multiplyCurrencies(
conversionRate,
@@ -167,37 +193,33 @@ function getSelectedTokenToFiatRate (state) {
return tokenToFiatRate
}
-function getSelectedTokenContract (state) {
- const selectedToken = getSelectedToken(state)
- return selectedToken
- ? global.eth.contract(abi).at(selectedToken.address)
- : null
+function getSendAmount (state) {
+ return state.metamask.send.amount
}
-function autoAddToBetaUI (state) {
- const autoAddTransactionThreshold = 12
- const autoAddAccountsThreshold = 2
- const autoAddTokensThreshold = 1
+function getSendEditingTransactionId (state) {
+ return state.metamask.send.editingTransactionId
+}
- const numberOfTransactions = state.metamask.selectedAddressTxList.length
- const numberOfAccounts = Object.keys(state.metamask.accounts).length
- const numberOfTokensAdded = state.metamask.tokens.length
+function getSendErrors (state) {
+ return state.send.errors
+}
- const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&
- (numberOfAccounts > autoAddAccountsThreshold) &&
- (numberOfTokensAdded > autoAddTokensThreshold)
- const userIsNotInBeta = !state.metamask.featureFlags.betaUI
+function getSendFrom (state) {
+ return state.metamask.send.from
+}
- return userIsNotInBeta && userPassesThreshold
+function getSendFromBalance (state) {
+ const from = getSendFrom(state) || getSelectedAccount(state)
+ return from.balance
}
-function getCurrentViewContext (state) {
- const { currentView = {} } = state.appState
- return currentView.context
+function getSendFromObject (state) {
+ return getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
}
-function getSendErrors (state) {
- return state.metamask.send.errors
+function getSendMaxModeState (state) {
+ return state.metamask.send.maxModeOn
}
function getSendTo (state) {
@@ -212,6 +234,38 @@ function getSendToAccounts (state) {
return Object.entries(allAccounts).map(([key, account]) => account)
}
-function getCurrentNetwork (state) {
- return state.metamask.network
-} \ No newline at end of file
+function getTokenBalance (state) {
+ return state.metamask.send.tokenBalance
+}
+
+function getTokenExchangeRate (state, tokenSymbol) {
+ const pair = `${tokenSymbol.toLowerCase()}_eth`
+ const tokenExchangeRates = state.metamask.tokenExchangeRates
+ const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
+
+ return tokenExchangeRate
+}
+
+function getUnapprovedTxs (state) {
+ return state.metamask.unapprovedTxs
+}
+
+function isSendFormInError (state) {
+ const { amount, to } = getSendErrors(state)
+ return Boolean(amount || to !== null)
+}
+
+function transactionsSelector (state) {
+ const { network, selectedTokenAddress } = state.metamask
+ const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
+ const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
+ const transactions = state.metamask.selectedAddressTxList || []
+ const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
+
+ return selectedTokenAddress
+ ? txsToRender
+ .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
+ .sort((a, b) => b.time - a.time)
+ : txsToRender
+ .sort((a, b) => b.time - a.time)
+}
diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js
index e69de29bb..e537d5624 100644
--- a/ui/app/components/send_/send.utils.js
+++ b/ui/app/components/send_/send.utils.js
@@ -0,0 +1,188 @@
+const {
+ addCurrencies,
+ conversionUtil,
+ conversionGTE,
+ multiplyCurrencies,
+} = require('../../conversion-util')
+const {
+ calcTokenAmount,
+} = require('../../token-util')
+const {
+ conversionGreaterThan,
+} = require('../../conversion-util')
+const {
+ INSUFFICIENT_FUNDS_ERROR,
+ INSUFFICIENT_TOKENS_ERROR,
+ NEGATIVE_ETH_ERROR,
+} = require('./send.constants')
+
+module.exports = {
+ calcGasTotal,
+ doesAmountErrorRequireUpdate,
+ generateTokenTransferData,
+ getAmountErrorObject,
+ getParamsForGasEstimate,
+ calcTokenBalance,
+ isBalanceSufficient,
+ isTokenBalanceSufficient,
+}
+
+function calcGasTotal (gasLimit, gasPrice) {
+ return multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ })
+}
+
+function isBalanceSufficient ({
+ amount = '0x0',
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal = '0x0',
+ primaryCurrency,
+}) {
+ const totalAmount = addCurrencies(amount, gasTotal, {
+ aBase: 16,
+ bBase: 16,
+ toNumericBase: 'hex',
+ })
+
+ const balanceIsSufficient = conversionGTE(
+ {
+ value: balance,
+ fromNumericBase: 'hex',
+ fromCurrency: primaryCurrency,
+ conversionRate,
+ },
+ {
+ value: totalAmount,
+ fromNumericBase: 'hex',
+ conversionRate: amountConversionRate || conversionRate,
+ fromCurrency: primaryCurrency,
+ },
+ )
+
+ return balanceIsSufficient
+}
+
+function isTokenBalanceSufficient ({
+ amount = '0x0',
+ tokenBalance,
+ decimals,
+}) {
+ const amountInDec = conversionUtil(amount, {
+ fromNumericBase: 'hex',
+ })
+
+ const tokenBalanceIsSufficient = conversionGTE(
+ {
+ value: tokenBalance,
+ fromNumericBase: 'dec',
+ },
+ {
+ value: calcTokenAmount(amountInDec, decimals),
+ fromNumericBase: 'dec',
+ },
+ )
+
+ return tokenBalanceIsSufficient
+}
+
+function getAmountErrorObject ({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+}) {
+ let insufficientFunds = false
+ if (gasTotal && conversionRate) {
+ insufficientFunds = !isBalanceSufficient({
+ amount: selectedToken ? '0x0' : amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ })
+ }
+
+ let inSufficientTokens = false
+ if (selectedToken && tokenBalance !== null) {
+ const { decimals } = selectedToken
+ inSufficientTokens = !isTokenBalanceSufficient({
+ tokenBalance,
+ amount,
+ decimals,
+ })
+ }
+
+ const amountLessThanZero = conversionGreaterThan(
+ { value: 0, fromNumericBase: 'dec' },
+ { value: amount, fromNumericBase: 'hex' },
+ )
+
+ let amountError = null
+
+ if (insufficientFunds) {
+ amountError = INSUFFICIENT_FUNDS_ERROR
+ } else if (inSufficientTokens) {
+ amountError = INSUFFICIENT_TOKENS_ERROR
+ } else if (amountLessThanZero) {
+ amountError = NEGATIVE_ETH_ERROR
+ }
+
+ return { amount: amountError }
+}
+
+function getParamsForGasEstimate (selectedAddress, symbol, data) {
+ const estimatedGasParams = {
+ from: selectedAddress,
+ gas: '746a528800',
+ }
+
+ if (symbol) {
+ Object.assign(estimatedGasParams, { value: '0x0' })
+ }
+
+ if (data) {
+ Object.assign(estimatedGasParams, { data })
+ }
+
+ return estimatedGasParams
+}
+
+function calcTokenBalance ({ selectedToken, usersToken }) {
+ const { decimals } = selectedToken || {}
+ return calcTokenAmount(usersToken.balance.toString(), decimals)
+}
+
+function doesAmountErrorRequireUpdate ({
+ balance,
+ gasTotal,
+ prevBalance,
+ prevGasTotal,
+ prevTokenBalance,
+ selectedToken,
+ tokenBalance,
+}) {
+ const balanceHasChanged = balance !== prevBalance
+ const gasTotalHasChange = gasTotal !== prevGasTotal
+ const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance
+ const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged
+
+ return amountErrorRequiresUpdate
+}
+
+function generateTokenTransferData (abi, selectedAddress, selectedToken) {
+ if (!selectedToken) return
+ return Array.prototype.map.call(
+ abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
+ x => ('00' + x.toString(16)).slice(-2)
+ ).join('')
+}