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

Client side error handling for from, to and amount fields in send.js

This commit is contained in:
Dan 2017-09-20 15:07:12 -02:30 committed by Chi Kei Chan
parent 83cda2b82e
commit 14bdc5a78c
4 changed files with 190 additions and 50 deletions

View File

@ -123,7 +123,20 @@ const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => {
}) })
} }
const conversionGreaterThan = (
{ value, fromNumericBase },
{ value: compareToValue, fromNumericBase: compareToBase },
) => {
const firstValue = converter({ value, fromNumericBase })
const secondValue = converter({
value: compareToValue,
fromNumericBase: compareToBase,
})
return firstValue.gt(secondValue)
}
module.exports = { module.exports = {
conversionUtil, conversionUtil,
addCurrencies, addCurrencies,
conversionGreaterThan,
} }

View File

@ -104,6 +104,16 @@
color: $red; color: $red;
} }
} }
.send-screen-input-wrapper__error-message {
display: block;
position: absolute;
bottom: 4px;
font-size: 12px;
line-height: 12px;
left: 8px;
color: $red;
}
} }
.send-screen-input { .send-screen-input {
@ -295,6 +305,11 @@
width: 163px; width: 163px;
text-align: center; text-align: center;
} }
&__send-button__disabled {
opacity: 0.5;
cursor: auto;
}
} }
.send-token { .send-token {

View File

@ -18,8 +18,8 @@ const {
signTx, signTx,
} = require('./actions') } = require('./actions')
const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util')
const { isHex, numericBalance, isValidAddress } = require('./util') const { isHex, numericBalance, isValidAddress, allNull } = require('./util')
const { conversionUtil } = require('./conversion-util') const { conversionUtil, conversionGreaterThan } = require('./conversion-util')
const BigNumber = require('bignumber.js') const BigNumber = require('bignumber.js')
module.exports = connect(mapStateToProps)(SendTransactionScreen) module.exports = connect(mapStateToProps)(SendTransactionScreen)
@ -51,7 +51,7 @@ function mapStateToProps (state) {
error: warning && warning.split('.')[0], error: warning && warning.split('.')[0],
account, account,
identity: identities[address], identity: identities[address],
balance: account ? numericBalance(account.balance) : null, balance: account ? account.balance : null,
} }
} }
@ -65,8 +65,8 @@ function SendTransactionScreen () {
newTx: { newTx: {
from: '', from: '',
to: '', to: '',
// these values are hardcoded, so "Next" can be clicked amount: 0,
amount: '0x0', // see L544 amountToSend: '0x0',
gasPrice: '0x5d21dba00', gasPrice: '0x5d21dba00',
gas: '0x7b0d', gas: '0x7b0d',
txData: null, txData: null,
@ -74,6 +74,8 @@ function SendTransactionScreen () {
}, },
activeCurrency: 'USD', activeCurrency: 'USD',
tooltipIsOpen: false, tooltipIsOpen: false,
errors: {},
isValid: false,
} }
this.back = this.back.bind(this) this.back = this.back.bind(this)
@ -81,12 +83,26 @@ function SendTransactionScreen () {
this.onSubmit = this.onSubmit.bind(this) this.onSubmit = this.onSubmit.bind(this)
this.setActiveCurrency = this.setActiveCurrency.bind(this) this.setActiveCurrency = this.setActiveCurrency.bind(this)
this.toggleTooltip = this.toggleTooltip.bind(this) this.toggleTooltip = this.toggleTooltip.bind(this)
this.validate = this.validate.bind(this)
this.getAmountToSend = this.getAmountToSend.bind(this)
this.setErrorsFor = this.setErrorsFor.bind(this)
this.clearErrorsFor = this.clearErrorsFor.bind(this)
this.renderFromInput = this.renderFromInput.bind(this) this.renderFromInput = this.renderFromInput.bind(this)
this.renderToInput = this.renderToInput.bind(this) this.renderToInput = this.renderToInput.bind(this)
this.renderAmountInput = this.renderAmountInput.bind(this) this.renderAmountInput = this.renderAmountInput.bind(this)
this.renderGasInput = this.renderGasInput.bind(this) this.renderGasInput = this.renderGasInput.bind(this)
this.renderMemoInput = this.renderMemoInput.bind(this) this.renderMemoInput = this.renderMemoInput.bind(this)
this.renderErrorMessage = this.renderErrorMessage.bind(this)
}
SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) {
const { errors } = this.state
const errorMessage = errors[errorType];
return errorMessage || warning
? h('div.send-screen-input-wrapper__error-message', [ errorMessage || warning ])
: null
} }
SendTransactionScreen.prototype.renderFromInput = function (from, identities) { SendTransactionScreen.prototype.renderFromInput = function (from, identities) {
@ -106,6 +122,8 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) {
}, },
}) })
}, },
onBlur: () => this.setErrorsFor('from'),
onFocus: () => this.clearErrorsFor('from'),
}), }),
h('datalist#accounts', [ h('datalist#accounts', [
@ -118,6 +136,8 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) {
}), }),
]), ]),
this.renderErrorMessage('from'),
]) ])
} }
@ -139,6 +159,8 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres
}, },
}) })
}, },
onBlur: () => this.setErrorsFor('to'),
onFocus: () => this.clearErrorsFor('to'),
}), }),
h('datalist#addresses', [ h('datalist#addresses', [
@ -160,6 +182,8 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres
}), }),
]), ]),
this.renderErrorMessage('to'),
]) ])
} }
@ -183,12 +207,17 @@ SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) {
this.state.newTx, this.state.newTx,
{ {
amount: event.target.value, amount: event.target.value,
amountToSend: this.getAmountToSend(event.target.value),
} }
), ),
}) })
}, },
onBlur: () => this.setErrorsFor('amount'),
onFocus: () => this.clearErrorsFor('amount'),
}), }),
this.renderErrorMessage('amount'),
]) ])
} }
@ -260,14 +289,13 @@ SendTransactionScreen.prototype.render = function () {
const props = this.props const props = this.props
const { const {
// selectedIdentity, warning,
// network,
identities, identities,
addressBook, addressBook,
conversionRate, conversionRate,
} = props } = props
const { blockGasLimit, newTx, activeCurrency } = this.state const { blockGasLimit, newTx, activeCurrency, isValid } = this.state
const { gas, gasPrice } = newTx const { gas, gasPrice } = newTx
return ( return (
@ -292,12 +320,15 @@ SendTransactionScreen.prototype.render = function () {
this.renderMemoInput(), this.renderMemoInput(),
this.renderErrorMessage(null, warning),
]), ]),
// Buttons underneath card // Buttons underneath card
h('section.flex-column.flex-center', [ h('section.flex-column.flex-center', [
h('button.btn-secondary.send-screen__send-button', { h('button.btn-secondary.send-screen__send-button', {
onClick: (event) => this.onSubmit(event), className: !isValid && 'send-screen__send-button__disabled',
onClick: (event) => isValid && this.onSubmit(event),
}, 'Next'), }, 'Next'),
h('button.btn-tertiary.send-screen__cancel-button', { h('button.btn-tertiary.send-screen__cancel-button', {
onClick: this.back, onClick: this.back,
@ -325,62 +356,140 @@ SendTransactionScreen.prototype.back = function () {
this.props.dispatch(backToAccountDetail(address)) this.props.dispatch(backToAccountDetail(address))
} }
SendTransactionScreen.prototype.validate = function (balance, amountToSend, { to, from }) {
const sufficientBalance = conversionGreaterThan(
{
value: balance,
fromNumericBase: 'hex',
},
{
value: amountToSend,
fromNumericBase: 'hex',
},
)
const amountLessThanZero = conversionGreaterThan(
{
value: 0,
fromNumericBase: 'dec',
},
{
value: amountToSend,
fromNumericBase: 'hex',
},
)
const errors = {}
if (!sufficientBalance) {
errors.amount = 'Insufficient funds.'
}
if (amountLessThanZero) {
errors.amount = 'Can not send negative amounts of ETH.'
}
if (!from) {
errors.from = 'Required'
}
if (from && !isValidAddress(from)) {
errors.from = 'Sender address is invalid.'
}
if (!to) {
errors.to = 'Required'
}
if (to && !isValidAddress(to)) {
errors.to = 'Recipient address is invalid.'
}
// if (txData && !isHex(stripHexPrefix(txData))) {
// message = 'Transaction data must be hex string.'
// return this.props.dispatch(displayWarning(message))
// }
return {
isValid: allNull(errors),
errors,
}
}
SendTransactionScreen.prototype.getAmountToSend = function (amount) {
const { activeCurrency } = this.state
const { conversionRate } = this.props
// TODO: need a clean way to integrate this into conversionUtil
const sendConversionRate = activeCurrency === 'ETH'
? conversionRate
: new BigNumber(1.0).div(conversionRate)
return conversionUtil(amount, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
fromCurrency: activeCurrency,
toCurrency: 'ETH',
toDenomination: 'WEI',
conversionRate: sendConversionRate,
})
}
SendTransactionScreen.prototype.setErrorsFor = function (field) {
const { balance } = this.props
const { newTx, errors: previousErrors } = this.state
const { amountToSend } = newTx
const {
isValid,
errors: newErrors
} = this.validate(balance, amountToSend, newTx)
const nextErrors = Object.assign({}, previousErrors, {
[field]: newErrors[field] || null
})
if (!isValid) {
this.setState({
errors: nextErrors,
isValid,
})
}
}
SendTransactionScreen.prototype.clearErrorsFor = function (field) {
const { errors: previousErrors } = this.state
const nextErrors = Object.assign({}, previousErrors, {
[field]: null
})
this.setState({
errors: nextErrors,
isValid: allNull(nextErrors),
})
}
SendTransactionScreen.prototype.onSubmit = function (event) { SendTransactionScreen.prototype.onSubmit = function (event) {
event.preventDefault() event.preventDefault()
const { warning } = this.props const { warning, balance, amountToSend } = this.props
const state = this.state || {} const state = this.state || {}
const recipient = state.newTx.to const recipient = state.newTx.to
const sender = state.newTx.from
const nickname = state.nickname || ' ' const nickname = state.nickname || ' '
// TODO: convert this to hex when created and include it in send // TODO: convert this to hex when created and include it in send
const txData = state.newTx.memo const txData = state.newTx.memo
let message
// if (value.gt(balance)) {
// message = 'Insufficient funds.'
// return this.props.dispatch(actions.displayWarning(message))
// }
// if (input < 0) {
// message = 'Can not send negative amounts of ETH.'
// return this.props.dispatch(actions.displayWarning(message))
// }
if (!isValidAddress(recipient) && !recipient) {
message = 'Recipient address is invalid.'
return this.props.dispatch(displayWarning(message))
}
if (txData && !isHex(stripHexPrefix(txData))) {
message = 'Transaction data must be hex string.'
return this.props.dispatch(displayWarning(message))
}
this.props.dispatch(hideWarning()) this.props.dispatch(hideWarning())
this.props.dispatch(addToAddressBook(recipient, nickname)) this.props.dispatch(addToAddressBook(recipient, nickname))
// TODO: need a clean way to integrate this into conversionUtil
const sendConversionRate = state.activeCurrency === 'ETH'
? this.props.conversionRate
: new BigNumber(1.0).div(this.props.conversionRate)
const sendAmount = conversionUtil(this.state.newTx.amount, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
fromCurrency: state.activeCurrency,
toCurrency: 'ETH',
toDenomination: 'WEI',
conversionRate: sendConversionRate,
})
var txParams = { var txParams = {
from: this.state.newTx.from, from: this.state.newTx.from,
to: this.state.newTx.to, to: this.state.newTx.to,
value: sendAmount, value: amountToSend,
gas: this.state.newTx.gas, gas: this.state.newTx.gas,
gasPrice: this.state.newTx.gasPrice, gasPrice: this.state.newTx.gasPrice,
@ -389,7 +498,5 @@ SendTransactionScreen.prototype.onSubmit = function (event) {
if (recipient) txParams.to = addHexPrefix(recipient) if (recipient) txParams.to = addHexPrefix(recipient)
if (txData) txParams.data = txData if (txData) txParams.data = txData
if (!warning) {
this.props.dispatch(signTx(txParams)) this.props.dispatch(signTx(txParams))
} }
}

View File

@ -55,6 +55,7 @@ module.exports = {
getContractAtAddress, getContractAtAddress,
exportAsFile: exportAsFile, exportAsFile: exportAsFile,
isInvalidChecksumAddress, isInvalidChecksumAddress,
allNull,
} }
function valuesFor (obj) { function valuesFor (obj) {
@ -273,3 +274,7 @@ function exportAsFile (filename, data) {
document.body.removeChild(elem) document.body.removeChild(elem)
} }
} }
function allNull (obj) {
return Object.entries(obj).every(([key, value]) => value === null)
}