1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Core of the refactor complete

This commit is contained in:
Dan 2018-04-26 14:08:38 -02:30
parent 02a6d2089e
commit 8ff7806f1b
34 changed files with 757 additions and 489 deletions

View File

@ -8,6 +8,7 @@ export default class PageContainer extends Component {
};
render () {
console.log(`QQQQQQQQQQQQQQQQQ this.props.children`, this.props.children);
return (
<div className="page-container">
{this.props.children}

View File

@ -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 {
className,
account,
handleClick,
icon = null,
conversionRate,
currentCurrency,
displayBalance = true,
displayAddress = false,
} = 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}
diameter={18}
className='account-list-item__identicon'
/>
<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
primaryCurrency='ETH'
convertedCurrency={currentCurrency}
value={balance}
conversionRate={conversionRate}
readOnly={true}
className='account-list-item__account-balances'
primaryBalanceClassName='account-list-item__account-primary-balance'
convertedBalanceClassName='account-list-item__account-secondary-balance'
/>}
</div>)
}
}
AccountListItem.contextTypes = {
t: PropTypes.func,
}

View File

@ -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),
}
}

View File

@ -31,7 +31,7 @@ export default class AmountMaxButton extends Component {
}
render () {
const { setMaxModeTo } = this.props
const { setMaxModeTo, maxModeOn } = this.props
return (
<div
@ -42,7 +42,7 @@ export default class AmountMaxButton extends Component {
this.setAmountToMax()
}}
>
{!maxModeOn ? this.context.t('max') : '' ])}
{!maxModeOn ? this.context.t('max') : ''}
</div>
);
}

View File

@ -1,3 +1,4 @@
import { connect } from 'react-redux'
import {
getSelectedToken,
getGasTotal,

View File

@ -1,15 +1,14 @@
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.component'
import AmountMaxButton from './amount-max-button/amount-max-button.component'
import CurrencyDisplay from '../../../send/currency-display'
export default class SendAmountRow extends Component {
static propTypes = {
amountConversionRate: PropTypes.string,
conversionRate: PropTypes.string,
from: PropTypes.object,
amountConversionRate: PropTypes.number,
conversionRate: PropTypes.number,
gasTotal: PropTypes.string,
primaryCurrency: PropTypes.string,
selectedToken: PropTypes.object,
@ -23,7 +22,7 @@ export default class SendAmountRow extends Component {
const {
amountConversionRate,
conversionRate,
from: { balance },
balance,
gasTotal,
primaryCurrency,
selectedToken,
@ -69,16 +68,16 @@ export default class SendAmountRow extends Component {
showError={inError}
errorType={'amount'}
>
!inError && gasTotal && <AmountMaxButton />
{!inError && gasTotal && <AmountMaxButton />}
<CurrencyDisplay
inError={inError},
primaryCurrency={primaryCurrency},
convertedCurrency={convertedCurrency},
selectedToken={selectedToken},
value={amount || '0x0'},
conversionRate={amountConversionRate},
handleChange={this.handleAmountChange},
>
inError={inError}
primaryCurrency={primaryCurrency}
convertedCurrency={convertedCurrency}
selectedToken={selectedToken}
value={amount || '0x0'}
conversionRate={amountConversionRate}
handleChange={newAmount => this.handleAmountChange(newAmount)}
/>
</SendRowWrapper>
);
}

View File

@ -1,41 +1,44 @@
import { connect } from 'react-redux'
import {
getSelectedToken,
getPrimaryCurrency,
getAmountConversionRate,
getConvertedCurrency,
getSendAmount,
getGasTotal,
getSelectedBalance,
getTokenBalance,
getSendFromBalance,
getConversionRate,
} from '../../send.selectors.js'
import {
getMaxModeOn,
sendAmountIsInError,
getPrimaryCurrency,
getAmountConversionRate,
} from './send-amount-row.selectors.js'
import { getAmountErrorObject } from './send-amount-row.utils.js'
import {
updateSendAmount,
setMaxModeTo,
} from '../../../actions'
updateSendErrors,
} from '../../../../actions'
import SendAmountRow from './send-amount-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow)
function mapStateToProps (state) {
updateSendTo
return {
selectedToken: getSelectedToken(state),
primaryCurrency: getPrimaryCurrency(state),
convertedCurrency: getConvertedCurrency(state),
amountConversionRate: getAmountConversionRate(state),
inError: sendAmountIsInError(state),
amount: getSendAmount(state),
maxModeOn: getMaxModeOn(state),
gasTotal: getGasTotal(state),
tokenBalance: getTokenBalance(state),
balance: getSendFromBalance(state),
}
return {
selectedToken: getSelectedToken(state),
primaryCurrency: getPrimaryCurrency(state),
convertedCurrency: getConvertedCurrency(state),
amountConversionRate: getAmountConversionRate(state),
inError: sendAmountIsInError(state),
amount: getSendAmount(state),
maxModeOn: getMaxModeOn(state),
gasTotal: getGasTotal(state),
tokenBalance: getTokenBalance(state),
balance: getSendFromBalance(state),
conversionRate: getConversionRate(state),
}
}
function mapDispatchToProps (dispatch) {

View File

@ -1,6 +1,14 @@
import {
getSelectedToken,
getSelectedTokenToFiatRate,
getConversionRate,
} from '../../send.selectors.js'
const selectors = {
getMaxModeOn,
sendAmountIsInError,
getPrimaryCurrency,
getAmountConversionRate,
}
module.exports = selectors
@ -10,5 +18,16 @@ function getMaxModeOn (state) {
}
function sendAmountIsInError (state) {
return Boolean(state.metamask.send.errors.amount)
return Boolean(state.metamask.send.errors.amount)
}
function getPrimaryCurrency (state) {
const selectedToken = getSelectedToken(state)
return selectedToken && selectedToken.symbol
}
function getAmountConversionRate (state) {
return Boolean(getSelectedToken(state))
? getSelectedTokenToFiatRate(state)
: getConversionRate(state)
}

View File

@ -1,4 +1,11 @@
const { isValidAddress } = require('../../../../util')
const {
conversionGreaterThan,
} = require('../../../../conversion-util')
const {
isBalanceSufficient,
isTokenBalanceSufficient,
} = require('../../send.utils')
function getAmountErrorObject ({
amount,
@ -10,6 +17,14 @@ function getAmountErrorObject ({
gasTotal,
tokenBalance,
}) {
console.log(`#& getAmountErrorObject amount`, amount);
console.log(`#& getAmountErrorObject balance`, balance);
console.log(`#& getAmountErrorObject amountConversionRate`, amountConversionRate);
console.log(`#& getAmountErrorObject conversionRate`, conversionRate);
console.log(`#& getAmountErrorObject primaryCurrency`, primaryCurrency);
console.log(`#& getAmountErrorObject selectedToken`, selectedToken);
console.log(`#& getAmountErrorObject gasTotal`, gasTotal);
console.log(`#& getAmountErrorObject tokenBalance`, tokenBalance);
let insufficientFunds = false
if (gasTotal && conversionRate) {
insufficientFunds = !isBalanceSufficient({
@ -40,11 +55,11 @@ function getAmountErrorObject ({
let amountError = null
if (insufficientFunds) {
amountError = this.context.t('insufficientFunds')
} else if (insufficientTokens) {
amountError = this.context.t('insufficientTokens')
amountError = 'insufficientFunds'
} else if (inSufficientTokens) {
amountError = 'insufficientTokens'
} else if (amountLessThanZero) {
amountError = this.context.t('negativeETH')
amountError = 'negativeETH'
}
return { amount: amountError }

View File

@ -1,13 +1,14 @@
import React, { Component } from 'react'
import PageContainerContent from '../../page-container/page-container-header.component'
import SendFromRow from './send-from-row/send-from-row.component'
import SendToRow from './send-to-row/send-to-row.component'
import SendAmountRow from './send-amount-row/send-amount-row.component'
import SendGasRow from './send-gas-row/send-gas-row.component'
import PageContainerContent from '../../page-container/page-container-content.component'
import SendFromRow from './send-from-row/send-from-row.container'
import SendToRow from './send-to-row/send-to-row.container'
import SendAmountRow from './send-amount-row/send-amount-row.container'
import SendGasRow from './send-gas-row/send-gas-row.container'
export default class SendContent extends Component {
render () {
console.log('111222333444555666777888999')
return (
<PageContainerContent>
<div className='.send-v2__form'>

View File

@ -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,
selectedAccount,
closeDropdown,
onSelect,
} = this.props
return (<div>
<div
className='send-v2__from-dropdown__close-area'
onClick={() => closeDropdown}
/>
<div className='send-v2__from-dropdown__list'>
{...accounts.map(account => <AccountListItem
className='account-list-item__dropdown'
account={account}
handleClick={() => {
onSelect(account)
closeDropdown()
}}
icon={this.getListItemIcon(account, selectedAccount.address)}
/>)}
</div>
</div>)
}
render () {
const {
selectedAccount,
openDropdown,
dropdownOpen,
} = this.props
console.log(`&*& openDropdown`, openDropdown);
console.log(`&*& dropdownOpen`, dropdownOpen);
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,
}

View File

@ -1,14 +1,14 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component'
import FromDropdown from '../../../send/from-dropdown'
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,
@ -41,7 +41,7 @@ export default class SendFromRow extends Component {
openFromDropdown,
closeFromDropdown,
} = this.props
console.log(`$% SendFromRow fromAccounts`, fromAccounts);
return (
<SendRowWrapper label={`${this.context.t('from')}:`}>
<FromDropdown

View File

@ -1,27 +1,30 @@
import { connect } from 'react-redux'
import {
getSendFrom,
getConversionRate,
getSelectedTokenContract,
getCurrentAccountWithSendEtherInfo,
accountsWithSendEtherInfoSelector,
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'
} from '../../../../actions'
import {
openFromDropdown,
closeFromDropdown,
} from '../../../ducks/send'
} from '../../../../ducks/send'
import SendFromRow from './send-from-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow)
function mapStateToProps (state) {
console.log(`$% mapStateToProps accountsWithSendEtherInfoSelector`, accountsWithSendEtherInfoSelector);
return {
from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state),
from: getSendFromObject(state),
fromAccounts: accountsWithSendEtherInfoSelector(state),
conversionRate: getConversionRate(state),
fromDropdownOpen: getFromDropdownOpen(state),
@ -38,7 +41,7 @@ function mapDispatchToProps (dispatch) {
dispatch(updateSendTokenBalance(tokenBalance))
},
updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)),
openFromDropdown: () => dispatch(()),
closeFromDropdown: () => dispatch(()),
openFromDropdown: () => dispatch(openFromDropdown()),
closeFromDropdown: () => dispatch(closeFromDropdown()),
}
}

View File

@ -1,6 +1,6 @@
const {
calcTokenAmount,
} = require('../../token-util')
} = require('../../../../token-util')
function calcTokenUpdateAmount (usersToken, selectedToken) {
const { decimals } = selectedToken || {}

View File

@ -7,7 +7,7 @@ export default class SendGasRow extends Component {
static propTypes = {
closeFromDropdown: PropTypes.func,
conversionRate: PropTypes.string,
conversionRate: PropTypes.number,
from: PropTypes.string,
fromAccounts: PropTypes.array,
fromDropdownOpen: PropTypes.bool,
@ -15,6 +15,7 @@ export default class SendGasRow extends Component {
tokenContract: PropTypes.object,
updateSendFrom: PropTypes.func,
updateSendTokenBalance: PropTypes.func,
gasLoadingError: PropTypes.bool,
};
async handleFromChange (newFrom) {
@ -43,11 +44,11 @@ export default class SendGasRow extends Component {
return (
<SendRowWrapper label={`${this.context.t('gasFee')}:`}>
<GasFeeDisplay
gasTotal={gasTotal},
conversionRate={conversionRate},
convertedCurrency={convertedCurrency},
onClick={() => showCustomizeGasModal()},
gasLoadingError={gasLoadingError},
gasTotal={gasTotal}
conversionRate={conversionRate}
convertedCurrency={convertedCurrency}
onClick={() => showCustomizeGasModal()}
gasLoadingError={gasLoadingError}
/>
</SendRowWrapper>
);

View File

@ -1,12 +1,12 @@
import { connect } from 'react-redux'
import {
getConversionRate,
getConvertedCurrency,
getGasTotal,
} from '../../send.selectors.js'
import { getGasLoadingError } from './send-gas-row.selectors.js'
import { calcTokenUpdateAmount } from './send-gas-row.utils.js'
import { showModal } from '../../../actions'
import SendGasRow from './send-from-row.component'
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)
@ -15,7 +15,7 @@ function mapStateToProps (state) {
conversionRate: getConversionRate(state),
convertedCurrency: getConvertedCurrency(state),
gasTotal: getGasTotal(state),
gasLoadingError: getGasLoadingError(state),
gasLoadingError: sendGasIsInError(state),
}
}

View File

@ -5,5 +5,5 @@ const selectors = {
module.exports = selectors
function sendGasIsInError (state) {
return state.send.errors.gasLoading
return state.metamask.send.errors.gasLoading
}

View File

@ -1,3 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class SendRowErrorMessage extends Component {
static propTypes = {
@ -11,7 +14,7 @@ export default class SendRowErrorMessage extends Component {
return (
errorMessage
? <div className='send-v2__error'>{errorMessage}</div>
? <div className='send-v2__error'>{this.context.t(errorMessage)}</div>
: null
);
}

View File

@ -1,3 +1,4 @@
import { connect } from 'react-redux'
import { getSendErrors } from '../../../send.selectors'
import SendRowErrorMessage from './send-row-error-message.component'

View File

@ -19,19 +19,14 @@ export default class SendRowWrapper extends Component {
children,
} = this.props
let formField = children[0]
let customLabelContent = null
if (children.length === 2) {
formField = children[1]
customLabelContent = children[0]
}
let formField = Array.isArray(children) ? children[1] || children[0] : children
let 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">

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component'
import ToDropdown from '../../../ens-input'
import EnsInput from '../../../ens-input'
export default class SendToRow extends Component {
@ -14,19 +14,20 @@ export default class SendToRow extends Component {
updateSendToError: PropTypes.func,
openToDropdown: PropTypes.func,
closeToDropdown: PropTypes.func,
network: PropTypes.number,
network: PropTypes.string,
};
handleToChange (to, nickname = '') {
const { updateSendTo, updateSendToError } = this.props
updateSendTo(to, nickname)
updateSendErrors(to)
updateSendToError(to)
}
render () {
const {
from,
fromAccounts,
toAccounts,
conversionRate,
fromDropdownOpen,
tokenContract,
@ -34,6 +35,8 @@ export default class SendToRow extends Component {
closeToDropdown,
network,
inError,
to,
toDropdownOpen,
} = this.props
return (
@ -44,14 +47,14 @@ export default class SendToRow extends Component {
>
<EnsInput
name={'address'}
placeholder={this.context.t('recipient Address')}
network={network},
to={to},
placeholder={this.context.t('recipientAddress')}
network={network}
to={to}
accounts={toAccounts}
dropdownOpen={toDropdownOpen}
openDropdown={() => openToDropdown()}
closeDropdown={() => closeToDropdown()}
onChange={this.handleToChange}
onChange={(newTo, newNickname) => this.handleToChange(newTo, newNickname)}
inError={inError}
/>
</SendRowWrapper>

View File

@ -1,7 +1,9 @@
import { connect } from 'react-redux'
import {
getSendTo,
getToAccounts,
getCurrentNetwork,
getSendToAccounts,
} from '../../send.selectors.js'
import {
getToDropdownOpen,
@ -11,11 +13,11 @@ import { getToErrorObject } from './send-to-row.utils.js'
import {
updateSendErrors,
updateSendTo,
} from '../../../actions'
} from '../../../../actions'
import {
openToDropdown,
closeToDropdown,
} from '../../../ducks/send'
} from '../../../../ducks/send'
import SendToRow from './send-to-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
@ -37,7 +39,7 @@ function mapDispatchToProps (dispatch) {
dispatch(updateSendErrors(getToErrorObject(to)))
},
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
openToDropdown: () => dispatch(()),
closeToDropdown: () => dispatch(()),
openToDropdown: () => dispatch(openToDropdown()),
closeToDropdown: () => dispatch(closeToDropdown()),
}
}

View File

@ -0,0 +1,93 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerFooter from '../../page-container/page-container-footer.component'
import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes'
export default class SendFooter extends Component {
static propTypes = {
addToAddressBook: PropTypes.func,
amount: PropTypes.string,
clearSend: PropTypes.func,
editingTransactionId: PropTypes.string,
errors: PropTypes.object,
from: PropTypes.object,
gasLimit: PropTypes.string,
gasPrice: PropTypes.string,
gasTotal: PropTypes.string,
history: PropTypes.object,
selectedToken: PropTypes.object,
signTokenTx: PropTypes.func,
signTx: PropTypes.func,
to: PropTypes.string,
toAccounts: PropTypes.array,
tokenBalance: PropTypes.string,
unapprovedTxs: PropTypes.object,
updateTx: 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({
from,
to,
amount,
gas,
gasPrice,
selectedToken,
editingTransactionId,
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,
}

View File

@ -0,0 +1,107 @@
import { connect } from 'react-redux'
import ethUtil from 'ethereumjs-util'
import {
addToAddressBook,
clearSend,
goHome,
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,
formShouldBeDisabled,
constructTxParams,
} from './send-footer.utils'
export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
function mapStateToProps (state) {
return {
isToken: Boolean(getSelectedToken(state)),
inError: isSendFormInError(state),
disabled: formShouldBeDisabled({
inError: isSendFormInError(state),
selectedToken: getSelectedToken(state),
tokenBalance: getTokenBalance(state),
gasTotal: getGasTotal(state),
}),
amount: getSendAmount(state),
editingTransactionId: getSendEditingTransactionId(state),
from: getSendFromObject(state),
gasLimit: getGasLimit(state),
gasPrice: getGasPrice(state),
selectedToken: getSelectedToken(state),
to: getSendTo(state),
unapprovedTxs: getUnapprovedTxs(state),
toAccounts: getSendToAccounts(state),
}
}
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(goHome()),
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))
}
}
}
}

View File

@ -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)
}

View File

@ -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,
}

View File

@ -0,0 +1,33 @@
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'
module.exports = {
MIN_GAS_PRICE_GWEI,
MIN_GAS_PRICE_HEX,
MIN_GAS_PRICE_DEC,
MIN_GAS_LIMIT_HEX,
MIN_GAS_LIMIT_DEC,
MIN_GAS_TOTAL,
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
}

View File

@ -2,14 +2,14 @@ import { valuesFor } from '../../util'
import abi from 'human-standard-token-abi'
import {
multiplyCurrencies,
} from './conversion-util'
} from '../../conversion-util'
const selectors = {
accountsWithSendEtherInfoSelector,
autoAddToBetaUI,
getConversionRate,
getAddressBook,
getConversionRate,
getConvertedCurrency,
getCurrentAccountWithSendEtherInfo,
getCurrentCurrency,
getCurrentNetwork,
@ -17,6 +17,7 @@ const selectors = {
getForceGasMin,
getGasLimit,
getGasPrice,
getGasTotal,
getSelectedAccount,
getSelectedAddress,
getSelectedIdentity,
@ -25,12 +26,18 @@ const selectors = {
getSelectedTokenExchangeRate,
getSelectedTokenToFiatRate,
getSendAmount,
getSendEditingTransactionId,
getSendErrors,
getSendFrom,
getSendFromObject,
getSendFromBalance,
getSendMaxModeState,
getSendTo,
getSendToAccounts,
getTokenBalance,
getTokenExchangeRate,
getUnapprovedTxs,
isSendFormInError,
transactionsSelector,
}
@ -84,10 +91,18 @@ function getTokenExchangeRate (state, tokenSymbol) {
return tokenExchangeRate
}
function getUnapprovedTxs (state) {
return state.metamask.unapprovedTxs
}
function getConversionRate (state) {
return state.metamask.conversionRate
}
function getConvertedCurrency (state) {
return state.metamask.currentCurrency
}
function getAddressBook (state) {
return state.metamask.addressBook
}
@ -97,11 +112,13 @@ function accountsWithSendEtherInfoSelector (state) {
accounts,
identities,
} = state.metamask
console.log(`accountsWithSendEtherInfoSelector accounts`, accounts);
console.log(`accountsWithSendEtherInfoSelector identities`, identities);
const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
return Object.assign({}, account, identities[key])
})
console.log(`accountsWithSendEtherInfoSelector accountsWithSendEtherInfo`, accountsWithSendEtherInfo);
return accountsWithSendEtherInfo
}
@ -132,6 +149,10 @@ function getGasPrice (state) {
return state.metamask.send.gasPrice
}
function getGasTotal (state) {
return state.metamask.send.gasTotal
}
function getGasLimit (state) {
return state.metamask.send.gasLimit
}
@ -144,8 +165,12 @@ function getSendFrom (state) {
return state.metamask.send.from
}
function getSendFromObject (state) {
return getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
}
function getSendFromBalance (state) {
const from = state.metamask.send.from || {}
const from = getSendFrom(state) || getSelectedAccount(state)
return from.balance
}
@ -203,6 +228,10 @@ function getCurrentViewContext (state) {
return currentView.context
}
function getSendEditingTransactionId (state) {
return state.metamask.send.editingTransactionId
}
function getSendErrors (state) {
return state.metamask.send.errors
}
@ -211,6 +240,10 @@ function getSendTo (state) {
return state.metamask.send.to
}
function getTokenBalance (state) {
return state.metamask.send.tokenBalance
}
function getSendToAccounts (state) {
const fromAccounts = accountsWithSendEtherInfoSelector(state)
const addressBookAccounts = getAddressBook(state)
@ -221,4 +254,9 @@ function getSendToAccounts (state) {
function getCurrentNetwork (state) {
return state.metamask.network
}
function isSendFormInError (state) {
const { amount, to } = getSendErrors(state)
return Boolean(amount || toError !== null)
}

View File

@ -0,0 +1,78 @@
const {
addCurrencies,
conversionUtil,
conversionGTE,
multiplyCurrencies,
} = require('../../conversion-util')
const {
calcTokenAmount,
} = require('../../token-util')
function isBalanceSufficient ({
amount = '0x0',
gasTotal = '0x0',
balance,
primaryCurrency,
amountConversionRate,
conversionRate,
}) {
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 getGasTotal (gasLimit, gasPrice) {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
}
module.exports = {
getGasTotal,
isBalanceSufficient,
isTokenBalanceSufficient,
}

View File

@ -10,10 +10,11 @@ const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN';
const initState = {
fromDropdownOpen: false,
toDropdownOpen: false,
errors: {},
}
// Reducer
export default function reducer(state = initState, action = {}) {
export default function reducer({ send: sendState = initState }, action = {}) {
switch (action.type) {
case OPEN_FROM_DROPDOWN:
return extend(sendState, {

View File

@ -8,7 +8,7 @@ const reduceIdentities = require('./reducers/identities')
const reduceMetamask = require('./reducers/metamask')
const reduceApp = require('./reducers/app')
const reduceLocale = require('./reducers/locale')
const reduceSend = require('./ducks/send')
const reduceSend = require('./ducks/send').default
window.METAMASK_CACHED_LOG_STATE = null

View File

@ -34,8 +34,8 @@ const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes')
import PageContainer from './components/page-container/page-container.component'
import SendHeader from './components/send_/send-header/send-header.container'
import PageContainerContent from './components/page-container/page-container-content.component'
import PageContainerFooter from './components/page-container/page-container-footer.component'
import SendContent from './components/send_/send-content/send-content.component'
import SendFooter from './components/send_/send-footer/send-footer.container'
SendTransactionScreen.contextTypes = {
t: PropTypes.func,
@ -57,8 +57,6 @@ function SendTransactionScreen () {
gasLoadingError: false,
}
this.handleToChange = this.handleToChange.bind(this)
this.handleAmountChange = this.handleAmountChange.bind(this)
this.validateAmount = this.validateAmount.bind(this)
}
@ -176,158 +174,6 @@ SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) {
}
}
SendTransactionScreen.prototype.renderHeader = function () {
const { selectedToken, clearSend, history } = this.props
return h('div.page-container__header', [
h('div.page-container__title', selectedToken ? this.context.t('sendTokens') : this.context.t('sendETH')),
h('div.page-container__subtitle', this.context.t('onlySendToEtherAddress')),
h('div.page-container__header-close', {
onClick: () => {
clearSend()
history.push(DEFAULT_ROUTE)
},
}),
])
}
SendTransactionScreen.prototype.renderErrorMessage = function (errorType) {
const { errors } = this.props
const errorMessage = errors[errorType]
return errorMessage
? h('div.send-v2__error', [ errorMessage ])
: null
}
SendTransactionScreen.prototype.handleFromChange = async function (newFrom) {
const {
updateSendFrom,
tokenContract,
} = this.props
if (tokenContract) {
const usersToken = await tokenContract.balanceOf(newFrom.address)
this.updateSendTokenBalance(usersToken)
}
updateSendFrom(newFrom)
}
SendTransactionScreen.prototype.renderFromRow = function () {
const {
from,
fromAccounts,
conversionRate,
} = this.props
const { fromDropdownOpen } = this.state
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', 'From:'),
h('div.send-v2__form-field', [
h(FromDropdown, {
dropdownOpen: fromDropdownOpen,
accounts: fromAccounts,
selectedAccount: from,
onSelect: newFrom => this.handleFromChange(newFrom),
openDropdown: () => this.setState({ fromDropdownOpen: true }),
closeDropdown: () => this.setState({ fromDropdownOpen: false }),
conversionRate,
}),
]),
])
}
SendTransactionScreen.prototype.handleToChange = function (to, nickname = '') {
const {
updateSendTo,
updateSendErrors,
} = this.props
let toError = null
if (!to) {
toError = this.context.t('required')
} else if (!isValidAddress(to)) {
toError = this.context.t('invalidAddressRecipient')
}
updateSendTo(to, nickname)
updateSendErrors({ to: toError })
}
SendTransactionScreen.prototype.renderToRow = function () {
const { toAccounts, errors, to, network } = this.props
const { toDropdownOpen } = this.state
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', [
this.context.t('to'),
this.renderErrorMessage(this.context.t('to')),
]),
h('div.send-v2__form-field', [
h(EnsInput, {
name: 'address',
placeholder: 'Recipient Address',
network,
to,
accounts: Object.entries(toAccounts).map(([key, account]) => account),
dropdownOpen: toDropdownOpen,
openDropdown: () => this.setState({ toDropdownOpen: true }),
closeDropdown: () => this.setState({ toDropdownOpen: false }),
onChange: this.handleToChange,
inError: Boolean(errors.to),
}),
]),
])
}
SendTransactionScreen.prototype.handleAmountChange = function (value) {
const amount = value
const { updateSendAmount, setMaxModeTo } = this.props
setMaxModeTo(false)
this.validateAmount(amount)
updateSendAmount(amount)
}
SendTransactionScreen.prototype.setAmountToMax = function () {
const {
from: { balance },
updateSendAmount,
updateSendErrors,
tokenBalance,
selectedToken,
gasTotal,
} = this.props
const { decimals } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0))
const maxAmount = selectedToken
? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'})
: subtractCurrencies(
ethUtil.addHexPrefix(balance),
ethUtil.addHexPrefix(gasTotal),
{ toNumericBase: 'hex' }
)
updateSendErrors({ amount: null })
updateSendAmount(maxAmount)
}
SendTransactionScreen.prototype.validateAmount = function (value) {
const {
@ -384,254 +230,19 @@ SendTransactionScreen.prototype.validateAmount = function (value) {
updateSendErrors({ amount: amountError })
}
SendTransactionScreen.prototype.renderAmountRow = function () {
const {
selectedToken,
primaryCurrency = 'ETH',
convertedCurrency,
amountConversionRate,
errors,
amount,
setMaxModeTo,
maxModeOn,
gasTotal,
} = this.props
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', [
'Amount:',
this.renderErrorMessage('amount'),
!errors.amount && gasTotal && h('div.send-v2__amount-max', {
onClick: (event) => {
event.preventDefault()
setMaxModeTo(true)
this.setAmountToMax()
},
}, [ !maxModeOn ? this.context.t('max') : '' ]),
]),
h('div.send-v2__form-field', [
h(CurrencyDisplay, {
inError: Boolean(errors.amount),
primaryCurrency,
convertedCurrency,
selectedToken,
value: amount || '0x0',
conversionRate: amountConversionRate,
handleChange: this.handleAmountChange,
}),
]),
])
}
SendTransactionScreen.prototype.renderGasRow = function () {
const {
conversionRate,
convertedCurrency,
showCustomizeGasModal,
gasTotal,
} = this.props
const { gasLoadingError } = this.state
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', this.context.t('gasFee')),
h('div.send-v2__form-field', [
h(GasFeeDisplay, {
gasTotal,
conversionRate,
convertedCurrency,
onClick: showCustomizeGasModal,
gasLoadingError,
}),
]),
])
}
SendTransactionScreen.prototype.renderMemoRow = function () {
const { updateSendMemo, memo } = this.props
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', 'Transaction Memo:'),
h('div.send-v2__form-field', [
h(MemoTextArea, {
memo,
onChange: (event) => updateSendMemo(event.target.value),
}),
]),
])
}
SendTransactionScreen.prototype.renderForm = function () {
return h(PageContainerContent, [
h('.send-v2__form', [
this.renderFromRow(),
this.renderToRow(),
this.renderAmountRow(),
this.renderGasRow(),
]),
])
}
SendTransactionScreen.prototype.renderFooter = function () {
const {
clearSend,
gasTotal,
tokenBalance,
selectedToken,
errors: { amount: amountError, to: toError },
history,
} = this.props
const missingTokenBalance = selectedToken && !tokenBalance
const noErrors = !amountError && toError === null
return h(PageContainerFooter, {
onCancel: () => {
clearSend()
history.push(DEFAULT_ROUTE)
},
onSubmit: e => this.onSubmit(e),
disabled: !noErrors || !gasTotal || missingTokenBalance,
})
}
SendTransactionScreen.prototype.render = function () {
const { history } = this.props
return (
h(PageContainer, [
h(SendHeader),
this.renderForm(),
h(SendContent),
this.renderFooter(),
h(SendFooter, { history }),
])
)
}
SendTransactionScreen.prototype.addToAddressBookIfNew = function (newAddress, nickname = '') {
const { toAccounts, addToAddressBook } = this.props
if (!toAccounts.find(({ address }) => newAddress === address)) {
// TODO: nickname, i.e. addToAddressBook(recipient, nickname)
addToAddressBook(newAddress, nickname)
}
}
SendTransactionScreen.prototype.getEditedTx = function () {
const {
from: {address: from},
to,
amount,
gasLimit: gas,
gasPrice,
selectedToken,
editingTransactionId,
unapprovedTxs,
} = this.props
const editingTx = {
...unapprovedTxs[editingTransactionId],
txParams: {
from: ethUtil.addHexPrefix(from),
gas: ethUtil.addHexPrefix(gas),
gasPrice: ethUtil.addHexPrefix(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, {
value: ethUtil.addHexPrefix('0'),
to: ethUtil.addHexPrefix(selectedToken.address),
data,
})
} else {
const { data } = unapprovedTxs[editingTransactionId].txParams
Object.assign(editingTx.txParams, {
value: ethUtil.addHexPrefix(amount),
to: ethUtil.addHexPrefix(to),
data,
})
if (typeof editingTx.txParams.data === 'undefined') {
delete editingTx.txParams.data
}
}
return editingTx
}
SendTransactionScreen.prototype.onSubmit = function (event) {
event.preventDefault()
const {
from: {address: from},
to: _to,
amount,
gasLimit: gas,
gasPrice,
signTokenTx,
signTx,
updateTx,
selectedToken,
editingTransactionId,
toNickname,
errors: { amount: amountError, to: toError },
} = this.props
const noErrors = !amountError && toError === null
if (!noErrors) {
return
}
const to = ethUtil.addHexPrefix(_to)
this.addToAddressBookIfNew(to, toNickname)
if (editingTransactionId) {
const editedTx = this.getEditedTx()
updateTx(editedTx)
} else {
const txParams = {
from,
value: '0',
gas,
gasPrice,
}
if (!selectedToken) {
txParams.value = amount
txParams.to = to
}
Object.keys(txParams).forEach(key => {
txParams[key] = ethUtil.addHexPrefix(txParams[key])
})
selectedToken
? signTokenTx(selectedToken.address, to, amount, txParams)
: signTx(txParams)
}
this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
}