aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/send-v2.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/send-v2.js')
-rw-r--r--ui/app/send-v2.js456
1 files changed, 440 insertions, 16 deletions
diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js
index 16964b45d..228cb22d0 100644
--- a/ui/app/send-v2.js
+++ b/ui/app/send-v2.js
@@ -3,8 +3,23 @@ const PropTypes = require('prop-types')
const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript')
+const ethAbi = require('ethereumjs-abi')
+const ethUtil = require('ethereumjs-util')
+
+const FromDropdown = require('./components/send/from-dropdown')
+const EnsInput = require('./components/ens-input')
+const CurrencyDisplay = require('./components/send/currency-display')
+const MemoTextArea = require('./components/send/memo-textarea')
+const GasFeeDisplay = require('./components/send/gas-fee-display-v2')
+
+const {
+ TOKEN_TRANSFER_FUNCTION_SIGNATURE,
+} = require('./components/send/send-constants')
+
const {
+ multiplyCurrencies,
conversionGreaterThan,
+ subtractCurrencies,
} = require('./conversion-util')
const {
calcTokenAmount,
@@ -14,11 +29,12 @@ const {
isTokenBalanceSufficient,
getGasTotal,
} = require('./components/send/send-utils')
+const { isValidAddress } = require('./util')
-import PageContainer from './components/page-container/page-container.component'
-import SendHeader from './components/send_/send-header/send-header.container'
-import SendContent from './components/send_/send-content/send-content.component'
-import SendFooter from './components/send_/send-footer/send-footer.container'
+import PageContainer from './components/page-container'
+// 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'
SendTransactionScreen.contextTypes = {
t: PropTypes.func,
@@ -40,6 +56,8 @@ function SendTransactionScreen () {
gasLoadingError: false,
}
+ this.handleToChange = this.handleToChange.bind(this)
+ this.handleAmountChange = this.handleAmountChange.bind(this)
this.validateAmount = this.validateAmount.bind(this)
}
@@ -74,6 +92,17 @@ SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) {
}
SendTransactionScreen.prototype.componentWillMount = function () {
+ const {
+ updateTokenExchangeRate,
+ selectedToken = {},
+ } = this.props
+
+ const { symbol } = selectedToken || {}
+
+ if (symbol) {
+ updateTokenExchangeRate(symbol)
+ }
+
this.updateGas()
}
@@ -84,7 +113,7 @@ SendTransactionScreen.prototype.updateGas = function () {
estimateGas,
selectedAddress,
data,
- setGasTotal,
+ updateGasTotal,
from,
tokenContract,
editingTransactionId,
@@ -110,7 +139,7 @@ SendTransactionScreen.prototype.updateGas = function () {
])
.then(([gasPrice, gas]) => {
const newGasTotal = getGasTotal(gas, gasPrice)
- setGasTotal(newGasTotal)
+ updateGasTotal(newGasTotal)
this.setState({ gasLoadingError: false })
})
.catch(err => {
@@ -118,7 +147,7 @@ SendTransactionScreen.prototype.updateGas = function () {
})
} else {
const newGasTotal = getGasTotal(gasLimit, gasPrice)
- setGasTotal(newGasTotal)
+ updateGasTotal(newGasTotal)
}
}
@@ -157,6 +186,139 @@ SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) {
}
}
+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 {
@@ -203,29 +365,291 @@ SendTransactionScreen.prototype.validateAmount = function (value) {
)
if (conversionRate && !sufficientBalance) {
- amountError = 'insufficientFunds'
+ amountError = this.context.t('insufficientFunds')
} else if (verifyTokenBalance && !sufficientTokens) {
- amountError = 'insufficientTokens'
+ amountError = this.context.t('insufficientTokens')
} else if (amountLessThanZero) {
- amountError = 'negativeETH'
+ amountError = this.context.t('negativeETH')
}
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 {
+ goHome,
+ clearSend,
+ gasTotal,
+ tokenBalance,
+ selectedToken,
+ errors: { amount: amountError, to: toError },
+ } = this.props
+
+ const missingTokenBalance = selectedToken && !tokenBalance
+ const noErrors = !amountError && toError === null
+
+ return h(PageContainerFooter, {
+ onCancel: () => {
+ clearSend()
+ goHome()
+ },
+ onSubmit: e => this.onSubmit(e),
+ disabled: !noErrors || !gasTotal || missingTokenBalance,
+ })
+}
+
SendTransactionScreen.prototype.render = function () {
- const { history } = this.props
+ const {
+ isToken,
+ clearSend,
+ goHome,
+ gasTotal,
+ tokenBalance,
+ selectedToken,
+ errors: { amount: amountError, to: toError },
+ } = this.props
+
+ const missingTokenBalance = selectedToken && !tokenBalance
+ const noErrors = !amountError && toError === null
return (
- h(PageContainer, [
+ h(PageContainer, {
+ title: isToken ? this.context.t('sendTokens') : this.context.t('sendETH'),
+ subtitle: this.context.t('onlySendToEtherAddress'),
+ onClose: () => {
+ clearSend()
+ goHome()
+ },
+ ContentComponent: this.renderForm,
+ onCancel: () => {
+ clearSend()
+ goHome()
+ },
+ onSubmit: e => this.onSubmit(e),
+ disabled: !noErrors || !gasTotal || missingTokenBalance,
+ })
+ // , [
- h(SendHeader),
+ // h(SendHeader),
- h(SendContent),
+ // this.renderForm(),
- h(SendFooter, { history }),
- ])
+ // this.renderFooter(),
+ // ])
)
}
+
+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)
+ }
+}