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

Merge pull request #2591 from MetaMask/NewUI-flat

Release 4.0.4
This commit is contained in:
Chi Kei Chan 2017-11-15 13:33:47 -08:00 committed by GitHub
commit b944a63ff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 779 additions and 179 deletions

View File

@ -2,9 +2,12 @@
## Current Master
## 3.12.0 2017-10-25
- Add support for alternative ENS TLDs (Ethereum Name Service Top-Level Domains).
- Lower minimum gas price to 0.1 GWEI.
- Remove web3 injection message from production (thanks to @ChainsawBaby)
- Add additional debugging info to our state logs, specifically OS version and browser version.
## 3.11.2 2017-10-21

View File

@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "4.0.3",
"version": "4.0.4",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",

View File

@ -133,7 +133,7 @@ module.exports = class TransactionController extends EventEmitter {
async newUnapprovedTransaction (txParams) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const txMeta = await this.addUnapprovedTransaction(txParams)
this.emit('newUnaprovedTx', txMeta)
this.emit('newUnapprovedTx', txMeta)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
@ -170,6 +170,7 @@ module.exports = class TransactionController extends EventEmitter {
async addTxDefaults (txMeta) {
const txParams = txMeta.txParams
// ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'

View File

@ -128,7 +128,7 @@ module.exports = class MetamaskController extends EventEmitter {
blockTracker: this.blockTracker,
ethQuery: this.ethQuery,
})
this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts))
this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
// computed balances (accounting for pending transactions)
this.balancesController = new BalancesController({

View File

@ -17,6 +17,15 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version
}
getPlatformInfo (cb) {
try {
extension.runtime.getPlatformInfo((platform) => {
cb(null, platform)
})
} catch (e) {
cb(e)
}
}
}
module.exports = ExtensionPlatform

View File

@ -118,8 +118,8 @@ describe('Transaction Controller', function () {
stub.restore()
})
it('should emit newUnaprovedTx event and pass txMeta as the first argument', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
it('should emit newUnapprovedTx event and pass txMeta as the first argument', function (done) {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
assert(txMetaFromEmit, 'txMeta is falsey')
assert.equal(txMetaFromEmit.id, 1, 'the right txMeta was passed')
done()
@ -129,7 +129,7 @@ describe('Transaction Controller', function () {
})
it('should resolve when finished and status is submitted and resolve with the hash', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
setTimeout(() => {
txController.setTxHash(txMetaFromEmit.id, '0x0')
txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id)
@ -145,7 +145,7 @@ describe('Transaction Controller', function () {
})
it('should reject when finished and status is rejected', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
setTimeout(() => {
txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id)
}, 10)

View File

@ -75,6 +75,7 @@ AccountImportSubview.prototype.render = function () {
}
}),
onChange: (opt) => {
props.dispatch(actions.showImportPage())
this.setState({ type: opt.value })
},
}),

View File

@ -115,6 +115,7 @@ var actions = {
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
NEXT_TX: 'NEXT_TX',
PREVIOUS_TX: 'PREV_TX',
EDIT_TX: 'EDIT_TX',
signMsg: signMsg,
cancelMsg: cancelMsg,
signPersonalMsg,
@ -129,10 +130,13 @@ var actions = {
completedTx: completedTx,
txError: txError,
nextTx: nextTx,
editTx,
previousTx: previousTx,
cancelAllTx: cancelAllTx,
viewPendingTx: viewPendingTx,
VIEW_PENDING_TX: 'VIEW_PENDING_TX',
updateTransactionParams,
UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
// send screen
estimateGas,
getGasPrice,
@ -140,19 +144,23 @@ var actions = {
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
UPDATE_SEND_FROM: 'UPDATE_SEND_FROM',
UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE',
UPDATE_SEND_TO: 'UPDATE_SEND_TO',
UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT',
UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO',
UPDATE_SEND_ERRORS: 'UPDATE_SEND_ERRORS',
UPDATE_SEND: 'UPDATE_SEND',
CLEAR_SEND: 'CLEAR_SEND',
updateGasLimit,
updateGasPrice,
updateGasTotal,
updateSendTokenBalance,
updateSendFrom,
updateSendTo,
updateSendAmount,
updateSendMemo,
updateSendErrors,
updateSend,
clearSend,
setSelectedAddress,
// app messages
@ -584,6 +592,13 @@ function updateGasTotal (gasTotal) {
}
}
function updateSendTokenBalance (tokenBalance) {
return {
type: actions.UPDATE_SEND_TOKEN_BALANCE,
value: tokenBalance,
}
}
function updateSendFrom (from) {
return {
type: actions.UPDATE_SEND_FROM,
@ -619,6 +634,13 @@ function updateSendErrors (error) {
}
}
function updateSend (newSend) {
return {
type: actions.UPDATE_SEND,
value: newSend,
}
}
function clearSend () {
return {
type: actions.CLEAR_SEND,
@ -659,6 +681,8 @@ function updateAndApproveTx (txData) {
log.debug(`actions calling background.updateAndApproveTx`)
background.updateAndApproveTransaction(txData, (err) => {
dispatch(actions.hideLoadingIndication())
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
dispatch(actions.clearSend())
if (err) {
dispatch(actions.txError(err))
dispatch(actions.goHome())
@ -676,6 +700,14 @@ function completedTx (id) {
}
}
function updateTransactionParams (id, txParams) {
return {
type: actions.UPDATE_TRANSACTION_PARAMS,
id,
value: txParams,
}
}
function txError (err) {
return {
type: actions.TRANSACTION_ERROR,
@ -939,6 +971,13 @@ function previousTx () {
}
}
function editTx (txId) {
return {
type: actions.EDIT_TX,
value: txId,
}
}
function showConfigPage (transitionForward = true) {
return {
type: actions.SHOW_CONFIG_PAGE,

View File

@ -0,0 +1,95 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = CurrencyInput
inherits(CurrencyInput, Component)
function CurrencyInput (props) {
Component.call(this)
this.state = {
value: sanitizeValue(props.value),
}
}
function removeNonDigits (str) {
return str.match(/\d|$/g).join('')
}
// Removes characters that are not digits, then removes leading zeros
function sanitizeInteger (val) {
return String(parseInt(removeNonDigits(val) || '0', 10))
}
function sanitizeDecimal (val) {
return removeNonDigits(val)
}
// Take a single string param and returns a non-negative integer or float as a string.
// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
// Removes leading zeros from the integer, and non-digits from the integer and decimal
// The integer is returned as '0' in cases where it would be empty. A decimal point is
// included in the returned string if one is included in the param
// Examples:
// sanitizeValue('0') -> '0'
// sanitizeValue('a') -> '0'
// sanitizeValue('010.') -> '10.'
// sanitizeValue('0.005') -> '0.005'
// sanitizeValue('22.200') -> '22.200'
// sanitizeValue('.200') -> '0.200'
// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
function sanitizeValue (value) {
let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value)
integer = sanitizeInteger(integer) || '0'
decimal = sanitizeDecimal(decimal)
return `${integer}${point}${decimal}`
}
CurrencyInput.prototype.handleChange = function (newValue) {
const { onInputChange } = this.props
this.setState({ value: sanitizeValue(newValue) })
onInputChange(sanitizeValue(newValue))
}
// If state.value === props.value plus a decimal point, or at least one
// zero or a decimal point and at least one zero, then this returns state.value
// after it is sanitized with getValueParts
CurrencyInput.prototype.getValueToRender = function () {
const { value } = this.props
const { value: stateValue } = this.state
const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue)
const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1])
return sanitizeValue(trailingDecimalAndZeroes
? stateValue
: value)
}
CurrencyInput.prototype.render = function () {
const {
className,
placeholder,
readOnly,
inputRef,
} = this.props
const inputSizeMultiplier = readOnly ? 1 : 1.2
const valueToRender = this.getValueToRender()
return h('input', {
className,
value: valueToRender,
placeholder,
size: valueToRender.length * inputSizeMultiplier,
readOnly,
onChange: e => this.handleChange(e.target.value),
ref: inputRef,
})
}

View File

@ -73,6 +73,8 @@ function getOriginalState (props) {
gasLimit,
gasTotal,
error: null,
priceSigZeros: '',
priceSigDec: '',
}
}
@ -107,7 +109,6 @@ CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
const {
amount,
balance,
primaryCurrency,
selectedToken,
amountConversionRate,
conversionRate,
@ -116,10 +117,9 @@ CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
let error = null
const balanceIsSufficient = isBalanceSufficient({
amount,
amount: selectedToken ? '0' : amount,
gasTotal,
balance,
primaryCurrency,
selectedToken,
amountConversionRate,
conversionRate,
@ -170,6 +170,13 @@ CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) {
CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
const { gasLimit } = this.state
const sigZeros = String(newGasPrice).match(/^\d+[.]\d*?(0+)$/)
const sigDec = String(newGasPrice).match(/^\d+([.])0*$/)
this.setState({
priceSigZeros: sigZeros && sigZeros[1] || '',
priceSigDec: sigDec && sigDec[1] || '',
})
const gasPrice = conversionUtil(newGasPrice, {
fromNumericBase: 'dec',
@ -191,15 +198,17 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
CustomizeGasModal.prototype.render = function () {
const { hideModal } = this.props
const { gasPrice, gasLimit, gasTotal, error } = this.state
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
const convertedGasPrice = conversionUtil(gasPrice, {
let convertedGasPrice = conversionUtil(gasPrice, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
toDenomination: 'GWEI',
})
convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}`
const convertedGasLimit = conversionUtil(gasLimit, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
@ -224,7 +233,7 @@ CustomizeGasModal.prototype.render = function () {
value: convertedGasPrice,
min: MIN_GAS_PRICE_GWEI,
// max: 1000,
step: MIN_GAS_PRICE_GWEI,
step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10),
onChange: value => this.convertAndSetGasPrice(value),
title: 'Gas Price (GWEI)',
copy: 'We calculate the suggested gas prices based on network success rates.',

View File

@ -31,7 +31,7 @@ class Dropdown extends Component {
containerClassName,
useCssTransition,
isOpen,
zIndex: 30,
zIndex: 55,
onClickOutside,
style,
innerStyle: innerStyleDefaults,

View File

@ -75,11 +75,12 @@ NetworkDropdown.prototype.render = function () {
}
},
containerClassName: 'network-droppo',
zIndex: 11,
zIndex: 55,
style: {
position: 'absolute',
top: '58px',
minWidth: '309px',
zIndex: '55px',
},
innerStyle: {
padding: '18px 8px',

View File

@ -1,11 +1,12 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const CurrencyInput = require('./currency-input')
const {
addCurrencies,
conversionGTE,
conversionLTE,
toNegative,
subtractCurrencies,
} = require('../conversion-util')
module.exports = InputNumber
@ -17,18 +18,24 @@ function InputNumber () {
this.setValue = this.setValue.bind(this)
}
function isValidInput (text) {
const re = /^([1-9]\d*|0)(\.|\.\d*)?$/
return re.test(text)
}
InputNumber.prototype.setValue = function (newValue) {
if (newValue && !isValidInput(newValue)) return
const { fixed, min = -1, max = Infinity, onChange } = this.props
newValue = Number(fixed ? newValue.toFixed(4) : newValue)
newValue = fixed ? newValue.toFixed(4) : newValue
const newValueGreaterThanMin = conversionGTE(
{ value: newValue, fromNumericBase: 'dec' },
{ value: newValue || '0', fromNumericBase: 'dec' },
{ value: min, fromNumericBase: 'hex' },
)
const newValueLessThanMax = conversionLTE(
{ value: newValue, fromNumericBase: 'dec' },
{ value: newValue || '0', fromNumericBase: 'dec' },
{ value: max, fromNumericBase: 'hex' },
)
if (newValueGreaterThanMin && newValueLessThanMax) {
@ -44,11 +51,13 @@ InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder, value = 0 } = this.props
return h('div.customize-gas-input-wrapper', {}, [
h('input.customize-gas-input', {
placeholder,
type: 'number',
h(CurrencyInput, {
className: 'customize-gas-input',
value,
onChange: (e) => this.setValue(e.target.value),
placeholder,
onInputChange: newValue => {
this.setValue(newValue)
},
}),
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [
@ -57,7 +66,7 @@ InputNumber.prototype.render = function () {
}),
h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' },
onClick: () => this.setValue(addCurrencies(value, toNegative(step))),
onClick: () => this.setValue(subtractCurrencies(value, step)),
}),
]),
])

View File

@ -10,20 +10,7 @@ class LoadingIndicator extends Component {
render () {
return (
h('.full-flex-height', {
style: {
left: '0px',
zIndex: 50,
position: 'absolute',
flexDirection: 'column',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
background: 'rgba(255, 255, 255, 0.8)',
},
}, [
h('.full-flex-height.loading-overlay', {}, [
h('img', {
src: 'images/loading.svg',
}),

View File

@ -3,6 +3,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const networkNames = require('../../../../app/scripts/config.js').networkNames
function mapStateToProps (state) {
return {
@ -22,6 +23,7 @@ function mapDispatchToProps (dispatch) {
showAccountDetailModal: () => {
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
},
toFaucet: network => dispatch(actions.buyEth({ network })),
}
}
@ -32,7 +34,20 @@ function BuyOptions () {
module.exports = connect(mapStateToProps, mapDispatchToProps)(BuyOptions)
BuyOptions.prototype.renderModalContentOption = function (title, header, onClick) {
return h('div.buy-modal-content-option', {
onClick,
}, [
h('div.buy-modal-content-option-title', {}, title),
h('div.buy-modal-content-option-subtitle', {}, header),
])
}
BuyOptions.prototype.render = function () {
const { network, toCoinbase, address, toFaucet } = this.props
const isTestNetwork = ['3', '4', '42'].find(n => n === network)
const networkName = networkNames[network]
return h('div', {}, [
h('div.buy-modal-content.transfers-subview', {
}, [
@ -47,27 +62,20 @@ BuyOptions.prototype.render = function () {
h('div.buy-modal-content-options.flex-column.flex-center', {}, [
h('div.buy-modal-content-option', {
onClick: () => {
const { toCoinbase, address } = this.props
toCoinbase(address)
},
}, [
h('div.buy-modal-content-option-title', {}, 'Coinbase'),
h('div.buy-modal-content-option-subtitle', {}, 'Deposit with Fiat'),
]),
isTestNetwork
? this.renderModalContentOption(networkName, 'Test Faucet', () => toFaucet(network))
: this.renderModalContentOption('Coinbase', 'Deposit with Fiat', () => toCoinbase(address)),
// h('div.buy-modal-content-option', {}, [
// h('div.buy-modal-content-option-title', {}, 'Shapeshift'),
// h('div.buy-modal-content-option-subtitle', {}, 'Trade any digital asset for any other'),
// ]),
h('div.buy-modal-content-option', {
onClick: () => this.goToAccountDetailsModal(),
}, [
h('div.buy-modal-content-option-title', {}, 'Direct Deposit'),
h('div.buy-modal-content-option-subtitle', {}, 'Deposit from another account'),
]),
this.renderModalContentOption(
'Direct Deposit',
'Deposit from another account',
() => this.goToAccountDetailsModal()
),
]),

View File

@ -189,7 +189,7 @@ const MODALS = {
}
const BACKDROPSTYLE = {
backgroundColor: 'rgba(245, 245, 245, 0.85)',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
}
function mapStateToProps (state) {

View File

@ -19,6 +19,7 @@ function mapStateToProps (state) {
conversionRate,
identities,
currentCurrency,
send,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
@ -27,12 +28,32 @@ function mapStateToProps (state) {
identities,
selectedAddress,
currentCurrency,
send,
}
}
function mapDispatchToProps (dispatch) {
return {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
clearSend: () => dispatch(actions.clearSend()),
editTransaction: txMeta => {
const { id, txParams } = txMeta
const {
gas: gasLimit,
gasPrice,
to,
value: amount,
} = txParams
dispatch(actions.updateSend({
gasLimit,
gasPrice,
gasTotal: null,
to,
amount,
errors: { to: null, amount: null },
editingTransactionId: id,
}))
dispatch(actions.showSendPage())
},
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
}
}
@ -157,7 +178,7 @@ ConfirmSendEther.prototype.getData = function () {
}
ConfirmSendEther.prototype.render = function () {
const { backToAccountDetail, selectedAddress, currentCurrency } = this.props
const { editTransaction, currentCurrency, clearSend } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
@ -199,8 +220,8 @@ ConfirmSendEther.prototype.render = function () {
h('div.confirm-screen-wrapper.flex-column.flex-grow', [
h('h3.flex-center.confirm-screen-header', [
h('button.confirm-screen-back-button', {
onClick: () => backToAccountDetail(selectedAddress),
}, 'BACK'),
onClick: () => editTransaction(txMeta),
}, 'EDIT'),
h('div.confirm-screen-title', 'Confirm Transaction'),
h('div.confirm-screen-header-tip'),
]),
@ -371,7 +392,10 @@ ConfirmSendEther.prototype.render = function () {
}, [
// Cancel Button
h('div.cancel.btn-light.confirm-screen-cancel-button', {
onClick: (event) => this.cancel(event, txMeta),
onClick: (event) => {
clearSend()
this.cancel(event, txMeta)
},
}, 'CANCEL'),
// Accept Button
@ -421,6 +445,26 @@ ConfirmSendEther.prototype.gatherTxMeta = function () {
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
if (props.send.editingTransactionId) {
const {
send: {
memo,
amount: value,
gasLimit: gas,
gasPrice,
},
} = props
const { txParams: { from, to } } = txData
txData.txParams = {
from: ethUtil.addHexPrefix(from),
to: ethUtil.addHexPrefix(to),
memo: memo && ethUtil.addHexPrefix(memo),
value: ethUtil.addHexPrefix(value),
gas: ethUtil.addHexPrefix(gas),
gasPrice: ethUtil.addHexPrefix(gasPrice),
}
}
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}

View File

@ -2,9 +2,10 @@ const Component = require('react').Component
const { connect } = require('react-redux')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const abi = require('human-standard-token-abi')
const ethAbi = require('ethereumjs-abi')
const tokenAbi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
abiDecoder.addABI(tokenAbi)
const actions = require('../../actions')
const clone = require('clone')
const Identicon = require('../identicon')
@ -15,12 +16,16 @@ const {
multiplyCurrencies,
addCurrencies,
} = require('../../conversion-util')
const {
calcTokenAmount,
} = require('../../token-util')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
const {
getTokenExchangeRate,
getSelectedAddress,
getSelectedTokenContract,
} = require('../../selectors')
module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmSendToken)
@ -29,6 +34,7 @@ function mapStateToProps (state, ownProps) {
const { token: { symbol }, txData } = ownProps
const { txParams } = txData || {}
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const {
conversionRate,
identities,
@ -44,6 +50,8 @@ function mapStateToProps (state, ownProps) {
tokenExchangeRate,
tokenData: tokenData || {},
currentCurrency: currentCurrency.toUpperCase(),
send: state.metamask.send,
tokenContract: getSelectedTokenContract(state),
}
}
@ -54,6 +62,33 @@ function mapDispatchToProps (dispatch, ownProps) {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
updateTokenExchangeRate: () => dispatch(actions.updateTokenExchangeRate(symbol)),
editTransaction: txMeta => {
const { token: { address } } = ownProps
const { txParams, id } = txMeta
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { params = [] } = tokenData
const { value } = params[1] || {}
const amount = conversionUtil(value, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
})
const {
gas: gasLimit,
gasPrice,
to,
} = txParams
dispatch(actions.setSelectedToken(address))
dispatch(actions.updateSend({
gasLimit,
gasPrice,
gasTotal: null,
to,
amount,
errors: { to: null, amount: null },
editingTransactionId: id,
}))
dispatch(actions.showSendTokenPage())
},
}
}
@ -65,16 +100,34 @@ function ConfirmSendToken () {
}
ConfirmSendToken.prototype.componentWillMount = function () {
const { tokenContract, selectedAddress } = this.props
tokenContract && tokenContract
.balanceOf(selectedAddress)
.then(usersToken => {
})
this.props.updateTokenExchangeRate()
}
ConfirmSendToken.prototype.getAmount = function () {
const { conversionRate, tokenExchangeRate, token, tokenData } = this.props
const {
conversionRate,
tokenExchangeRate,
token,
tokenData,
send: { amount, editingTransactionId },
} = this.props
const { params = [] } = tokenData
const { value } = params[1] || {}
let { value } = params[1] || {}
const { decimals } = token
const multiplier = Math.pow(10, Number(decimals || 0))
const sendTokenAmount = Number(value / multiplier)
if (editingTransactionId) {
value = conversionUtil(amount, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
}
const sendTokenAmount = calcTokenAmount(value, decimals)
return {
fiat: tokenExchangeRate
@ -240,9 +293,8 @@ ConfirmSendToken.prototype.renderTotalPlusGas = function () {
}
ConfirmSendToken.prototype.render = function () {
const { backToAccountDetail, selectedAddress } = this.props
const { editTransaction } = this.props
const txMeta = this.gatherTxMeta()
const {
from: {
address: fromAddress,
@ -264,8 +316,8 @@ ConfirmSendToken.prototype.render = function () {
h('div.confirm-screen-wrapper.flex-column.flex-grow', [
h('h3.flex-center.confirm-screen-header', [
h('button.confirm-screen-back-button', {
onClick: () => backToAccountDetail(selectedAddress),
}, 'BACK'),
onClick: () => editTransaction(txMeta),
}, 'EDIT'),
h('div.confirm-screen-title', 'Confirm Transaction'),
h('div.confirm-screen-header-tip'),
]),
@ -387,6 +439,38 @@ ConfirmSendToken.prototype.gatherTxMeta = function () {
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
if (props.send.editingTransactionId) {
const {
send: {
memo,
amount,
gasLimit: gas,
gasPrice,
},
} = props
const { txParams: { from, to } } = txData
const tokenParams = {
from: ethUtil.addHexPrefix(from),
value: '0',
gas: ethUtil.addHexPrefix(gas),
gasPrice: ethUtil.addHexPrefix(gasPrice),
}
const data = '0xa9059cbb' + Array.prototype.map.call(
ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]),
x => ('00' + x.toString(16)).slice(-2)
).join('')
txData.txParams = {
...tokenParams,
to: ethUtil.addHexPrefix(to),
memo: memo && ethUtil.addHexPrefix(memo),
data,
}
}
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}

View File

@ -22,6 +22,7 @@ module.exports = connect(mapStateToProps)(AccountListItem)
AccountListItem.prototype.render = function () {
const {
className,
account,
handleClick,
icon = null,
@ -34,6 +35,7 @@ AccountListItem.prototype.render = function () {
const { name, address, balance } = account || {}
return h('div.account-list-item', {
className,
onClick: () => handleClick({ name, address, balance }),
}, [

View File

@ -1,6 +1,7 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const CurrencyInput = require('../currency-input')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
module.exports = CurrencyDisplay
@ -8,15 +9,6 @@ module.exports = CurrencyDisplay
inherits(CurrencyDisplay, Component)
function CurrencyDisplay () {
Component.call(this)
this.state = {
value: null,
}
}
function isValidInput (text) {
const re = /^([1-9]\d*|0)(\.|\.\d*)?$/
return re.test(text)
}
function toHexWei (value) {
@ -39,6 +31,28 @@ CurrencyDisplay.prototype.getAmount = function (value) {
: toHexWei(value)
}
CurrencyDisplay.prototype.getValueToRender = function () {
const { selectedToken, conversionRate, value } = this.props
const { decimals, symbol } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0))
return selectedToken
? conversionUtil(value, {
fromNumericBase: 'hex',
toCurrency: symbol,
conversionRate: multiplier,
invertConversionRate: true,
})
: conversionUtil(value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
numberOfDecimals: 6,
conversionRate,
})
}
CurrencyDisplay.prototype.render = function () {
const {
className = 'currency-display',
@ -49,64 +63,41 @@ CurrencyDisplay.prototype.render = function () {
convertedCurrency,
readOnly = false,
inError = false,
value: initValue,
handleChange,
validate,
} = this.props
const { value } = this.state
const initValueToRender = conversionUtil(initValue, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromDenomination: 'WEI',
numberOfDecimals: 6,
conversionRate,
})
const valueToRender = this.getValueToRender()
const convertedValue = conversionUtil(value || initValueToRender, {
let convertedValue = conversionUtil(valueToRender, {
fromNumericBase: 'dec',
fromCurrency: primaryCurrency,
toCurrency: convertedCurrency,
numberOfDecimals: 2,
conversionRate,
})
const inputSizeMultiplier = readOnly ? 1 : 1.2
convertedValue = Number(convertedValue).toFixed(2)
return h('div', {
className,
style: {
borderColor: inError ? 'red' : null,
},
onClick: () => this.currencyInput.focus(),
}, [
h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [
h('input', {
h(CurrencyInput, {
className: primaryBalanceClassName,
value: `${value || initValueToRender}`,
value: `${valueToRender}`,
placeholder: '0',
size: (value || initValueToRender).length * inputSizeMultiplier,
readOnly,
onChange: (event) => {
let newValue = event.target.value
if (newValue === '') {
newValue = '0'
} else if (newValue.match(/^0[1-9]$/)) {
newValue = newValue.match(/[1-9]/)[0]
}
if (newValue && !isValidInput(newValue)) {
event.preventDefault()
} else {
validate(this.getAmount(newValue))
this.setState({ value: newValue })
}
onInputChange: newValue => {
handleChange(this.getAmount(newValue))
},
onBlur: event => !readOnly && handleChange(this.getAmount(event.target.value)),
inputRef: input => { this.currencyInput = input },
}),
h('span.currency-display__currency-symbol', primaryCurrency),

View File

@ -35,6 +35,7 @@ FromDropdown.prototype.renderDropdown = function () {
h('div.send-v2__from-dropdown__list', {}, [
...accounts.map(account => h(AccountListItem, {
className: 'account-list-item__dropdown',
account,
handleClick: () => {
onSelect(account)

View File

@ -11,6 +11,7 @@ const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX
toDenomination: 'GWEI',
fromNumericBase: 'hex',
toNumericBase: 'hex',
numberOfDecimals: 1,
}))
const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, {

View File

@ -1,11 +1,17 @@
const { addCurrencies, conversionGreaterThan } = require('../../conversion-util')
const {
addCurrencies,
conversionUtil,
conversionGTE,
} = require('../../conversion-util')
const {
calcTokenAmount,
} = require('../../token-util')
function isBalanceSufficient ({
amount,
gasTotal,
amount = '0x0',
gasTotal = '0x0',
balance,
primaryCurrency,
selectedToken,
amountConversionRate,
conversionRate,
}) {
@ -15,7 +21,7 @@ function isBalanceSufficient ({
toNumericBase: 'hex',
})
const balanceIsSufficient = conversionGreaterThan(
const balanceIsSufficient = conversionGTE(
{
value: balance,
fromNumericBase: 'hex',
@ -26,13 +32,37 @@ function isBalanceSufficient ({
value: totalAmount,
fromNumericBase: 'hex',
conversionRate: amountConversionRate,
fromCurrency: selectedToken || primaryCurrency,
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
}
module.exports = {
isBalanceSufficient,
isTokenBalanceSufficient,
}

View File

@ -13,6 +13,7 @@ const {
getSendFrom,
getCurrentCurrency,
getSelectedTokenToFiatRate,
getSelectedTokenContract,
} = require('../../selectors')
module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther)
@ -48,6 +49,7 @@ function mapStateToProps (state) {
convertedCurrency: getCurrentCurrency(state),
data,
amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate,
tokenContract: getSelectedTokenContract(state),
}
}
@ -61,9 +63,13 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
),
signTx: txParams => dispatch(actions.signTx(txParams)),
updateAndApproveTx: txParams => dispatch(actions.updateAndApproveTx(txParams)),
setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)),
addToAddressBook: address => dispatch(actions.addToAddressBook(address)),
updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)),
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)),
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)),
updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)),
updateSendTo: newTo => dispatch(actions.updateSendTo(newTo)),
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
@ -71,5 +77,6 @@ function mapDispatchToProps (dispatch) {
updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)),
goHome: () => dispatch(actions.goHome()),
clearSend: () => dispatch(actions.clearSend()),
backToConfirmScreen: editingTransactionId => dispatch(actions.showConfTxPage({ id: editingTransactionId })),
}
}

View File

@ -38,6 +38,7 @@ ToAutoComplete.prototype.renderDropdown = function () {
...accountsToRender.map(account => h(AccountListItem, {
account,
className: 'account-list-item__dropdown',
handleClick: () => {
onChange(account.address)
closeDropdown()
@ -88,7 +89,7 @@ ToAutoComplete.prototype.render = function () {
inError,
} = this.props
return h('div.to-autocomplete', {}, [
return h('div.send-v2__to-autocomplete', {}, [
h('input.send-v2__to-autocomplete__input', {
placeholder: 'Recipient Address',

View File

@ -151,10 +151,7 @@ TokenList.prototype.componentDidUpdate = function (nextProps) {
}
TokenList.prototype.updateBalances = function (tokens) {
const heldTokens = tokens.filter(token => {
return token.balance !== '0' && token.string !== '0.000'
})
this.setState({ tokens: heldTokens, isLoading: false })
this.setState({ tokens, isLoading: false })
}
TokenList.prototype.componentWillUnmount = function () {

View File

@ -10,6 +10,7 @@ const Identicon = require('./identicon')
const contractMap = require('eth-contract-metadata')
const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
const { calcTokenAmount } = require('../token-util')
const { getCurrentCurrency } = require('../selectors')
@ -135,8 +136,7 @@ TxListItem.prototype.getSendTokenTotal = async function () {
const { params = [] } = decodedData || {}
const { value } = params[1] || {}
const { decimals, symbol } = await this.getTokenInfo()
const multiplier = Math.pow(10, Number(decimals || 0))
const total = Number(value / multiplier)
const total = calcTokenAmount(value, decimals)
const pair = symbol && `${symbol.toLowerCase()}_eth`

View File

@ -37,7 +37,7 @@ const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
// Individual Setters
const convert = R.invoker(1, 'times')
const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN)
const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN)
const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate)
const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
@ -53,7 +53,7 @@ const toNormalizedDenomination = {
}
const toSpecifiedDenomination = {
WEI: bigNumber => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(),
GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(1),
GWEI: bigNumber => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9),
}
const baseChange = {
hex: n => n.toString(16),
@ -145,6 +145,20 @@ const addCurrencies = (a, b, options = {}) => {
})
}
const subtractCurrencies = (a, b, options = {}) => {
const {
aBase,
bBase,
...conversionOptions
} = options
const value = (new BigNumber(a, aBase)).minus(b, bBase)
return converter({
value,
...conversionOptions,
})
}
const multiplyCurrencies = (a, b, options = {}) => {
const {
multiplicandBase,
@ -169,6 +183,7 @@ const conversionGreaterThan = (
) => {
const firstValue = converter({ ...firstProps })
const secondValue = converter({ ...secondProps })
return firstValue.gt(secondValue)
}
@ -202,4 +217,5 @@ module.exports = {
conversionGTE,
conversionLTE,
toNegative,
subtractCurrencies,
}

View File

@ -69,4 +69,15 @@
overflow: hidden;
text-overflow: ellipsis;
}
&__dropdown {
&:hover {
background: rgba($alto, .2);
cursor: pointer;
input {
background: rgba($alto, .1);
}
}
}
}

View File

@ -16,6 +16,8 @@
@import './confirm.scss';
@import './loading-overlay.scss';
// Balances
@import './hero-balance.scss';

View File

@ -0,0 +1,21 @@
.loading-overlay {
left: 0px;
z-index: 50;
position: absolute;
flex-direction: column;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
background: rgba(255, 255, 255, 0.8);
@media screen and (max-width: 575px) {
margin-top: 56px;
height: calc(100% - 56px);
}
@media screen and (min-width: 576px) {
margin-top: 75px;
height: calc(100% - 75px);
}
}

View File

@ -577,6 +577,7 @@
line-height: 16px;
font-size: 12px;
color: $tundora;
position: relative;
&__close-area {
position: fixed;
@ -591,7 +592,7 @@
z-index: 1050;
position: absolute;
height: 220px;
width: 240px;
width: 100%;
border: 1px solid $geyser;
border-radius: 4px;
background-color: $white;
@ -628,6 +629,15 @@
}
}
&__amount-max {
color: $curious-blue;
font-family: Roboto;
font-size: 12px;
left: 8px;
border: none;
cursor: pointer;
}
&__gas-fee-display {
width: 100%;
}

View File

@ -1,4 +1,5 @@
const extend = require('xtend')
const copyToClipboard = require('copy-to-clipboard')
//
// Sub-Reducers take in the complete state and return their sub-state
@ -41,17 +42,33 @@ function rootReducer (state, action) {
return state
}
window.logState = function () {
window.logStateString = function (cb) {
const state = window.METAMASK_CACHED_LOG_STATE
let version
try {
version = global.platform.getVersion()
} catch (e) {
version = 'unable to load version.'
}
state.version = version
const stateString = JSON.stringify(state, removeSeedWords, 2)
return stateString
const version = global.platform.getVersion()
const browser = window.navigator.userAgent
return global.platform.getPlatformInfo((err, platform) => {
if (err) {
return cb(err)
}
state.version = version
state.platform = platform
state.browser = browser
const stateString = JSON.stringify(state, removeSeedWords, 2)
return cb(null, stateString)
})
}
window.logState = function (toClipboard) {
return window.logStateString((err, result) => {
if (err) {
console.error(err.message)
} else if (toClipboard) {
copyToClipboard(result)
console.log('State log copied')
} else {
console.log(result)
}
})
}
function removeSeedWords (key, value) {

View File

@ -175,6 +175,7 @@ function reduceApp (state, action) {
name: 'import-menu',
},
transForward: true,
warning: null,
})
case actions.SHOW_INFO_PAGE:

View File

@ -27,11 +27,13 @@ function reduceMetamask (state, action) {
gasLimit: null,
gasPrice: null,
gasTotal: null,
tokenBalance: null,
from: '',
to: '',
amount: '0x0',
memo: '',
errors: {},
editingTransactionId: null,
},
coinOptions: {},
}, state.metamask)
@ -107,6 +109,14 @@ function reduceMetamask (state, action) {
}
return newState
case actions.EDIT_TX:
return extend(metamaskState, {
send: {
...metamaskState.send,
editingTransactionId: action.value,
},
})
case actions.SHOW_NEW_VAULT_SEED:
return extend(metamaskState, {
isUnlocked: true,
@ -140,9 +150,9 @@ function reduceMetamask (state, action) {
case actions.SAVE_ACCOUNT_LABEL:
const account = action.value.account
const name = action.value.label
var id = {}
const id = {}
id[account] = extend(metamaskState.identities[account], { name })
var identities = extend(metamaskState.identities, id)
const identities = extend(metamaskState.identities, id)
return extend(metamaskState, { identities })
case actions.SET_CURRENT_FIAT:
@ -196,6 +206,14 @@ function reduceMetamask (state, action) {
},
})
case actions.UPDATE_SEND_TOKEN_BALANCE:
return extend(metamaskState, {
send: {
...metamaskState.send,
tokenBalance: action.value,
},
})
case actions.UPDATE_SEND_FROM:
return extend(metamaskState, {
send: {
@ -239,20 +257,44 @@ function reduceMetamask (state, action) {
},
})
case actions.UPDATE_SEND:
return extend(metamaskState, {
send: {
...metamaskState.send,
...action.value,
},
})
case actions.CLEAR_SEND:
return extend(metamaskState, {
send: {
gasLimit: null,
gasPrice: null,
gasTotal: null,
tokenBalance: null,
from: '',
to: '',
amount: '0x0',
memo: '',
errors: {},
editingTransactionId: null,
},
})
case actions.UPDATE_TRANSACTION_PARAMS:
const { id: txId, value } = action
let { selectedAddressTxList } = metamaskState
selectedAddressTxList = selectedAddressTxList.map(tx => {
if (tx.id === txId) {
tx.txParams = value
}
return tx
})
return extend(metamaskState, {
selectedAddressTxList,
})
case actions.PAIR_UPDATE:
const { value: { marketinfo: pairMarketInfo } } = action
return extend(metamaskState, {

View File

@ -1,4 +1,5 @@
const valuesFor = require('./util').valuesFor
const abi = require('human-standard-token-abi')
const {
multiplyCurrencies,
@ -22,6 +23,7 @@ const selectors = {
getCurrentCurrency,
getSendAmount,
getSelectedTokenToFiatRate,
getSelectedTokenContract,
}
module.exports = selectors
@ -149,3 +151,10 @@ function getSelectedTokenToFiatRate (state) {
return tokenToFiatRate
}
function getSelectedTokenContract (state) {
const selectedToken = getSelectedToken(state)
return selectedToken
? global.eth.contract(abi).at(selectedToken.address)
: null
}

View File

@ -2,6 +2,8 @@ const { inherits } = require('util')
const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript')
const ethUtil = require('ethereumjs-util')
const Identicon = require('./components/identicon')
const FromDropdown = require('./components/send/from-dropdown')
const ToAutoComplete = require('./components/send/to-autocomplete')
@ -9,15 +11,24 @@ const CurrencyDisplay = require('./components/send/currency-display')
const MemoTextArea = require('./components/send/memo-textarea')
const GasFeeDisplay = require('./components/send/gas-fee-display-v2')
const { MIN_GAS_TOTAL } = require('./components/send/send-constants')
const {
MIN_GAS_TOTAL,
MIN_GAS_PRICE_HEX,
MIN_GAS_LIMIT_HEX,
} = require('./components/send/send-constants')
const {
multiplyCurrencies,
conversionGreaterThan,
subtractCurrencies,
} = require('./conversion-util')
const {
calcTokenAmount,
} = require('./token-util')
const {
isBalanceSufficient,
} = require('./components/send/send-utils.js')
isTokenBalanceSufficient,
} = require('./components/send/send-utils')
const { isValidAddress } = require('./util')
module.exports = SendTransactionScreen
@ -40,6 +51,36 @@ function SendTransactionScreen () {
this.validateAmount = this.validateAmount.bind(this)
}
const getParamsForGasEstimate = function (selectedAddress, symbol, data) {
const estimatedGasParams = {
from: selectedAddress,
gas: '746a528800',
}
if (symbol) {
Object.assign(estimatedGasParams, { value: '0x0' })
}
if (data) {
Object.assign(estimatedGasParams, { data })
}
return estimatedGasParams
}
SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) {
if (!usersToken) return
const {
selectedToken = {},
updateSendTokenBalance,
} = this.props
const { decimals } = selectedToken || {}
const tokenBalance = calcTokenAmount(usersToken.balance.toString(), decimals)
updateSendTokenBalance(tokenBalance)
}
SendTransactionScreen.prototype.componentWillMount = function () {
const {
updateTokenExchangeRate,
@ -49,40 +90,75 @@ SendTransactionScreen.prototype.componentWillMount = function () {
selectedAddress,
data,
updateGasTotal,
from,
tokenContract,
editingTransactionId,
gasPrice,
gasLimit,
} = this.props
const { symbol } = selectedToken || {}
const estimateGasParams = {
from: selectedAddress,
gas: '746a528800',
}
if (symbol) {
updateTokenExchangeRate(symbol)
Object.assign(estimateGasParams, { value: '0x0' })
}
if (data) {
Object.assign(estimateGasParams, { data })
}
const estimateGasParams = getParamsForGasEstimate(selectedAddress, symbol, data)
Promise
.all([
getGasPrice(),
estimateGas({
from: selectedAddress,
gas: '746a528800',
}),
])
.then(([gasPrice, gas]) => {
const tokenBalancePromise = tokenContract && tokenContract.balanceOf(from.address)
let newGasTotal
if (!editingTransactionId) {
Promise
.all([
getGasPrice(),
estimateGas(estimateGasParams),
tokenBalancePromise,
])
.then(([gasPrice, gas, usersToken]) => {
const newGasTotal = multiplyCurrencies(gas, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
const newGasTotal = multiplyCurrencies(gas, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
updateGasTotal(newGasTotal)
this.updateSendTokenBalance(usersToken)
})
updateGasTotal(newGasTotal)
} else {
newGasTotal = multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
updateGasTotal(newGasTotal)
tokenBalancePromise && tokenBalancePromise.then(
usersToken => this.updateSendTokenBalance(usersToken))
}
}
SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) {
const {
from: { balance },
gasTotal,
tokenBalance,
amount,
selectedToken,
} = this.props
const {
from: { balance: prevBalance },
gasTotal: prevGasTotal,
tokenBalance: prevTokenBalance,
} = prevProps
const notFirstRender = [prevBalance, prevGasTotal].every(n => n !== null)
const balanceHasChanged = balance !== prevBalance
const gasTotalHasChange = gasTotal !== prevGasTotal
const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance
const amountValidationChange = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged
if (notFirstRender && amountValidationChange) {
this.validateAmount(amount)
}
}
SendTransactionScreen.prototype.renderHeaderIcon = function () {
@ -144,12 +220,24 @@ SendTransactionScreen.prototype.renderErrorMessage = function (errorType) {
: 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,
updateSendFrom,
} = this.props
const { fromDropdownOpen } = this.state
@ -163,7 +251,7 @@ SendTransactionScreen.prototype.renderFromRow = function () {
dropdownOpen: fromDropdownOpen,
accounts: fromAccounts,
selectedAccount: from,
onSelect: updateSendFrom,
onSelect: newFrom => this.handleFromChange(newFrom),
openDropdown: () => this.setState({ fromDropdownOpen: true }),
closeDropdown: () => this.setState({ fromDropdownOpen: false }),
conversionRate,
@ -227,9 +315,41 @@ SendTransactionScreen.prototype.handleAmountChange = function (value) {
const amount = value
const { updateSendAmount } = this.props
this.validateAmount(amount)
updateSendAmount(amount)
}
SendTransactionScreen.prototype.setAmountToMax = function () {
const {
from: { balance },
updateSendAmount,
updateSendErrors,
updateGasPrice,
updateGasLimit,
updateGasTotal,
tokenBalance,
selectedToken,
} = 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(MIN_GAS_TOTAL),
{ toNumericBase: 'hex' }
)
updateSendErrors({ amount: null })
if (!selectedToken) {
updateGasPrice(MIN_GAS_PRICE_HEX)
updateGasLimit(MIN_GAS_LIMIT_HEX)
updateGasTotal(MIN_GAS_TOTAL)
}
updateSendAmount(maxAmount)
}
SendTransactionScreen.prototype.validateAmount = function (value) {
const {
from: { balance },
@ -239,21 +359,30 @@ SendTransactionScreen.prototype.validateAmount = function (value) {
primaryCurrency,
selectedToken,
gasTotal,
tokenBalance,
} = this.props
const { decimals } = selectedToken || {}
const amount = value
let amountError = null
const sufficientBalance = isBalanceSufficient({
amount,
amount: selectedToken ? '0x0' : amount,
gasTotal,
balance,
primaryCurrency,
selectedToken,
amountConversionRate,
conversionRate,
})
let sufficientTokens
if (selectedToken) {
sufficientTokens = isTokenBalanceSufficient({
tokenBalance,
amount,
decimals,
})
}
const amountLessThanZero = conversionGreaterThan(
{ value: 0, fromNumericBase: 'dec' },
{ value: amount, fromNumericBase: 'hex' },
@ -261,6 +390,8 @@ SendTransactionScreen.prototype.validateAmount = function (value) {
if (!sufficientBalance) {
amountError = 'Insufficient funds.'
} else if (selectedToken && !sufficientTokens) {
amountError = 'Insufficient tokens.'
} else if (amountLessThanZero) {
amountError = 'Can not send negative amounts of ETH.'
}
@ -275,15 +406,20 @@ SendTransactionScreen.prototype.renderAmountRow = function () {
convertedCurrency,
amountConversionRate,
errors,
amount,
} = this.props
const { amount } = this.state
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', [
'Amount:',
this.renderErrorMessage('amount'),
!errors.amount && h('div.send-v2__amount-max', {
onClick: (event) => {
event.preventDefault()
this.setAmountToMax()
},
}, [ 'Max' ]),
]),
h('div.send-v2__form-field', [
@ -292,10 +428,9 @@ SendTransactionScreen.prototype.renderAmountRow = function () {
primaryCurrency,
convertedCurrency,
selectedToken,
value: amount,
value: amount || '0x0',
conversionRate: amountConversionRate,
handleChange: this.handleAmountChange,
validate: this.validateAmount,
}),
]),
@ -335,8 +470,7 @@ SendTransactionScreen.prototype.renderGasRow = function () {
}
SendTransactionScreen.prototype.renderMemoRow = function () {
const { updateSendMemo } = this.props
const { memo } = this.state
const { updateSendMemo, memo } = this.props
return h('div.send-v2__form-row', [
@ -383,7 +517,7 @@ SendTransactionScreen.prototype.renderFooter = function () {
errors: { amount: amountError, to: toError },
} = this.props
const noErrors = amountError === null && toError === null
const noErrors = !amountError && toError === null
const errorClass = noErrors ? '' : '__disabled'
return h('div.send-v2__footer', [
@ -433,11 +567,12 @@ SendTransactionScreen.prototype.onSubmit = function (event) {
signTokenTx,
signTx,
selectedToken,
clearSend,
editingTransactionId,
errors: { amount: amountError, to: toError },
backToConfirmScreen,
} = this.props
const noErrors = amountError === null && toError === null
const noErrors = !amountError && toError === null
if (!noErrors) {
return
@ -445,6 +580,11 @@ SendTransactionScreen.prototype.onSubmit = function (event) {
this.addToAddressBookIfNew(to)
if (editingTransactionId) {
backToConfirmScreen(editingTransactionId)
return
}
const txParams = {
from,
value: '0',
@ -457,8 +597,6 @@ SendTransactionScreen.prototype.onSubmit = function (event) {
txParams.to = to
}
clearSend()
selectedToken
? signTokenTx(selectedToken.address, to, amount, txParams)
: signTx(txParams)

0
ui/app/token-tracker.js Normal file
View File

View File

@ -31,6 +31,15 @@ const tokenInfoGetter = function () {
}
}
function calcTokenAmount (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
const amount = Number(value / multiplier)
return amount
}
module.exports = {
tokenInfoGetter,
calcTokenAmount,
}

View File

@ -8379,6 +8379,10 @@ redux-logger@^3.0.6:
dependencies:
deep-diff "^0.3.5"
redux-test-utils@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/redux-test-utils/-/redux-test-utils-0.1.3.tgz#0d89100f100f86c7c7214976eaece88e7e45bf74"
redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"