diff options
Merge branch 'master' into AddTokenList
Diffstat (limited to 'ui/app/components')
-rw-r--r-- | ui/app/components/balance.js | 3 | ||||
-rw-r--r-- | ui/app/components/bn-as-decimal-input.js | 174 | ||||
-rw-r--r-- | ui/app/components/buy-button-subview.js | 147 | ||||
-rw-r--r-- | ui/app/components/coinbase-form.js | 114 | ||||
-rw-r--r-- | ui/app/components/copyable.js | 46 | ||||
-rw-r--r-- | ui/app/components/custom-radio-list.js | 60 | ||||
-rw-r--r-- | ui/app/components/drop-menu-item.js | 5 | ||||
-rw-r--r-- | ui/app/components/ens-input.js | 26 | ||||
-rw-r--r-- | ui/app/components/eth-balance.js | 16 | ||||
-rw-r--r-- | ui/app/components/fiat-value.js | 20 | ||||
-rw-r--r-- | ui/app/components/hex-as-decimal-input.js | 2 | ||||
-rw-r--r-- | ui/app/components/identicon.js | 19 | ||||
-rw-r--r-- | ui/app/components/network.js | 14 | ||||
-rw-r--r-- | ui/app/components/notice.js | 7 | ||||
-rw-r--r-- | ui/app/components/pending-tx.js | 194 | ||||
-rw-r--r-- | ui/app/components/qr-code.js | 12 | ||||
-rw-r--r-- | ui/app/components/shapeshift-form.js | 34 | ||||
-rw-r--r-- | ui/app/components/shift-list-item.js | 12 | ||||
-rw-r--r-- | ui/app/components/transaction-list-item-icon.js | 18 | ||||
-rw-r--r-- | ui/app/components/transaction-list-item.js | 31 | ||||
-rw-r--r-- | ui/app/components/transaction-list.js | 3 |
21 files changed, 646 insertions, 311 deletions
diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js index 3c5e24b65..57ca84564 100644 --- a/ui/app/components/balance.js +++ b/ui/app/components/balance.js @@ -1,7 +1,8 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance const generateBalanceObject = require('../util').generateBalanceObject +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 6810303e1..87084f92d 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -6,12 +6,15 @@ const actions = require('../actions') const CoinbaseForm = require('./coinbase-form') const ShapeshiftForm = require('./shapeshift-form') const Loading = require('./loading') -const TabBar = require('./tab-bar') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') module.exports = connect(mapStateToProps)(BuyButtonSubview) function mapStateToProps (state) { return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], warning: state.appState.warning, buyView: state.appState.buyView, network: state.metamask.network, @@ -31,7 +34,11 @@ BuyButtonSubview.prototype.render = function () { const isLoading = props.isSubLoading return ( - h('.buy-eth-section', [ + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ // back button h('.flex-row', { style: { @@ -46,58 +53,79 @@ BuyButtonSubview.prototype.render = function () { left: '10px', }, }), - h('h2.page-subtitle', 'Buy Eth'), - ]), - - h(Loading, { isLoading }), - - h(TabBar, { - tabs: [ - { - content: [ - 'Coinbase', - h('a', { - onClick: (event) => this.navigateTo('https://github.com/MetaMask/faq/blob/master/COINBASE.md'), - }, [ - h('i.fa.fa-question-circle', { - style: { - margin: '0px 5px', - }, - }), - ]), - ], - key: 'coinbase', + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', }, - { - content: [ - 'Shapeshift', - h('a', { - href: 'https://github.com/MetaMask/faq/blob/master/COINBASE.md', - onClick: (event) => this.navigateTo('https://info.shapeshift.io/about'), - }, [ - h('i.fa.fa-question-circle', { - style: { - margin: '0px 5px', - }, - }), - ]), - ], - key: 'shapeshift', + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', }, - ], - defaultTab: 'coinbase', - tabSelected: (key) => { - switch (key) { - case 'coinbase': - props.dispatch(actions.coinBaseSubview()) - break - case 'shapeshift': - props.dispatch(actions.shapeShiftSubview(props.provider.type)) - break - } + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', }, - }), - + }, props.buyView.subview), this.formVersionSubview(), ]) ) @@ -124,13 +152,19 @@ BuyButtonSubview.prototype.formVersionSubview = function () { marginBottom: '15px', }, }, 'In order to access this feature, please switch to the Main Network'), - ((network === '3') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, (network === '3') ? h('button.text-transform-uppercase', { onClick: () => this.props.dispatch(actions.buyEth({ network })), style: { marginTop: '15px', }, }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, (network === '42') ? h('button.text-transform-uppercase', { onClick: () => this.props.dispatch(actions.buyEth({ network })), style: { @@ -152,3 +186,12 @@ BuyButtonSubview.prototype.backButtonContext = function () { this.props.dispatch(actions.goHome()) } } + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/coinbase-form.js index fd5816a21..f44d86045 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/app/components/coinbase-form.js @@ -4,7 +4,6 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../actions') -const isValidAddress = require('../util').isValidAddress module.exports = connect(mapStateToProps)(CoinbaseForm) function mapStateToProps (state) { @@ -21,105 +20,36 @@ function CoinbaseForm () { CoinbaseForm.prototype.render = function () { var props = this.props - var amount = props.buyView.amount - var address = props.buyView.buyAddress return h('.flex-column', { style: { - // margin: '10px', + marginTop: '35px', padding: '25px', + width: '100%', }, }, [ - h('.flex-column', { - style: { - alignItems: 'flex-start', - }, - }, [ - h('.flex-row', [ - h('div', 'Address:'), - h('.ellip-address', address), - ]), - h('.flex-row', [ - h('div', 'Amount: $'), - h('.input-container', [ - h('input.buy-inputs', { - style: { - width: '3em', - boxSizing: 'border-box', - }, - defaultValue: amount, - onChange: this.handleAmount.bind(this), - }), - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '5px', - right: '11px', - }, - }), - ]), - ]), - ]), - - h('.info-gray', { - style: { - fontSize: '10px', - fontFamily: 'Montserrat Light', - margin: '15px', - lineHeight: '13px', - }, - }, - `there is a USD$ 15 a day max and a USD$ 50 - dollar limit per the life time of an account without a - coinbase account. A fee of 3.75% will be aplied to debit/credit cards.`), - - !props.warning ? h('div', { - style: { - width: '340px', - height: '22px', - }, - }) : props.warning && h('span.error.flex-center', props.warning), - - h('.flex-row', { style: { justifyContent: 'space-around', margin: '33px', + marginTop: '0px', }, }, [ - h('button', { + h('button.btn-green', { onClick: this.toCoinbase.bind(this), }, 'Continue to Coinbase'), - h('button', { + h('button.btn-red', { onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), }, 'Cancel'), ]), ]) } -CoinbaseForm.prototype.handleAmount = function (event) { - this.props.dispatch(actions.updateCoinBaseAmount(event.target.value)) -} -CoinbaseForm.prototype.handleAddress = function (event) { - this.props.dispatch(actions.updateBuyAddress(event.target.value)) -} -CoinbaseForm.prototype.toCoinbase = function () { - var props = this.props - var amount = props.buyView.amount - var address = props.buyView.buyAddress - var message - if (isValidAddress(address) && isValidAmountforCoinBase(amount).valid) { - props.dispatch(actions.buyEth({ network: '1', address, amount: props.buyView.amount })) - } else if (!isValidAmountforCoinBase(amount).valid) { - message = isValidAmountforCoinBase(amount).message - return props.dispatch(actions.displayWarning(message)) - } else { - message = 'Receiving address is invalid.' - return props.dispatch(actions.displayWarning(message)) - } +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) } CoinbaseForm.prototype.renderLoading = function () { @@ -131,29 +61,3 @@ CoinbaseForm.prototype.renderLoading = function () { src: 'images/loading.svg', }) } - -function isValidAmountforCoinBase (amount) { - amount = parseFloat(amount) - if (amount) { - if (amount <= 15 && amount > 0) { - return { - valid: true, - } - } else if (amount > 15) { - return { - valid: false, - message: 'The amount can not be greater then $15', - } - } else { - return { - valid: false, - message: 'Can not buy amounts less then $0', - } - } - } else { - return { - valid: false, - message: 'The amount entered is not a number', - } - } -} diff --git a/ui/app/components/copyable.js b/ui/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/ui/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/app/components/custom-radio-list.js b/ui/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/ui/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/ui/app/components/drop-menu-item.js b/ui/app/components/drop-menu-item.js index 3eb6ec876..e42948209 100644 --- a/ui/app/components/drop-menu-item.js +++ b/ui/app/components/drop-menu-item.js @@ -42,11 +42,14 @@ DropMenuItem.prototype.activeNetworkRender = function () { if (providerType === 'mainnet') return h('.check', '✓') break case 'Ropsten Test Network': - if (providerType === 'testnet') return h('.check', '✓') + if (providerType === 'ropsten') return h('.check', '✓') break case 'Kovan Test Network': if (providerType === 'kovan') return h('.check', '✓') break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break case 'Localhost 8545': if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') break diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index facf29d97..43bb7ab22 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -5,11 +5,9 @@ const extend = require('xtend') const debounce = require('debounce') const copyToClipboard = require('copy-to-clipboard') const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') const ensRE = /.+\.eth$/ -const networkResolvers = { - '3': '112234455c3a32fd11230c42e7bccd4a84e02010', -} module.exports = EnsInput @@ -23,9 +21,10 @@ EnsInput.prototype.render = function () { const opts = extend(props, { list: 'addresses', onChange: () => { + this.setState({ ensResolution: '0x0000000000000000000000000000000000000000' }) const network = this.props.network - let resolverAddress = networkResolvers[network] - if (!resolverAddress) return + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return const recipient = document.querySelector('input[name="address"]').value if (recipient.match(ensRE) === null) { @@ -52,7 +51,7 @@ EnsInput.prototype.render = function () { [ // Corresponds to the addresses owned. Object.keys(props.identities).map((key) => { - let identity = props.identities[key] + const identity = props.identities[key] return h('option', { value: identity.address, label: identity.name, @@ -63,6 +62,7 @@ EnsInput.prototype.render = function () { return h('option', { value: identity.address, label: identity.name, + key: identity.address, }) }), ]), @@ -72,10 +72,10 @@ EnsInput.prototype.render = function () { EnsInput.prototype.componentDidMount = function () { const network = this.props.network - let resolverAddress = networkResolvers[network] + const networkHasEnsSupport = getNetworkEnsSupport(network) - if (resolverAddress) { - const provider = web3.currentProvider + if (networkHasEnsSupport) { + const provider = global.ethereumProvider this.ens = new ENS({ provider, network }) this.checkName = debounce(this.lookupEnsName.bind(this), 200) } @@ -96,12 +96,14 @@ EnsInput.prototype.lookupEnsName = function () { log.info(`ENS attempting to resolve name: ${recipient}`) this.ens.lookup(recipient.trim()) .then((address) => { + if (address === '0x0000000000000000000000000000000000000000') throw new Error('No address has been set for this name.') if (address !== ensResolution) { this.setState({ loadingEns: false, ensResolution: address, nickname: recipient.trim(), hoverText: address + '\nClick to Copy', + ensFailure: false, }) } }) @@ -109,6 +111,7 @@ EnsInput.prototype.lookupEnsName = function () { log.error(reason) return this.setState({ loadingEns: false, + ensResolution: '0x0000000000000000000000000000000000000000', ensFailure: true, hoverText: reason.message, }) @@ -168,3 +171,8 @@ EnsInput.prototype.ensIconContents = function (recipient) { }) } } + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} + diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index 57ca84564..4f538fd31 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -16,20 +16,19 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props - var style = props.style + const { style, width } = props var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true value = value ? formatBalance(value, 6, needsParse) : '...' - var width = props.width return ( h('.ether-balance.ether-balance-amount', { - style: style, + style, }, [ h('div', { style: { display: 'inline', - width: width, + width, }, }, this.renderBalance(value)), ]) @@ -38,16 +37,17 @@ EthBalanceComponent.prototype.render = function () { } EthBalanceComponent.prototype.renderBalance = function (value) { var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props if (value === 'None') return value if (value === '...') return value - var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) var balance var splitBalance = value.split(' ') var ethNumber = splitBalance[0] var ethSuffix = splitBalance[1] const showFiat = 'showFiat' in props ? props.showFiat : true - if (props.shorten) { + if (shorten) { balance = balanceObj.shortBalance } else { balance = balanceObj.balance @@ -73,7 +73,7 @@ EthBalanceComponent.prototype.renderBalance = function (value) { width: '100%', textAlign: 'right', }, - }, this.props.incoming ? `+${balance}` : balance), + }, incoming ? `+${balance}` : balance), h('div', { style: { color: ' #AEAEAE', @@ -83,7 +83,7 @@ EthBalanceComponent.prototype.renderBalance = function (value) { }, label), ]), - showFiat ? h(FiatValue, { value: props.value }) : null, + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, ])) ) } diff --git a/ui/app/components/fiat-value.js b/ui/app/components/fiat-value.js index 298809b30..8a64a1cfc 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/fiat-value.js @@ -1,17 +1,9 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const connect = require('react-redux').connect const formatBalance = require('../util').formatBalance -module.exports = connect(mapStateToProps)(FiatValue) - -function mapStateToProps (state) { - return { - conversionRate: state.metamask.conversionRate, - currentCurrency: state.metamask.currentCurrency, - } -} +module.exports = FiatValue inherits(FiatValue, Component) function FiatValue () { @@ -20,23 +12,23 @@ function FiatValue () { FiatValue.prototype.render = function () { const props = this.props + const { conversionRate, currentCurrency } = props + const value = formatBalance(props.value, 6) if (value === 'None') return value var fiatDisplayNumber, fiatTooltipNumber var splitBalance = value.split(' ') - if (props.conversionRate !== 0) { - fiatTooltipNumber = Number(splitBalance[0]) * props.conversionRate + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate fiatDisplayNumber = fiatTooltipNumber.toFixed(2) } else { fiatDisplayNumber = 'N/A' fiatTooltipNumber = 'Unknown' } - var fiatSuffix = props.currentCurrency - - return fiatDisplay(fiatDisplayNumber, fiatSuffix) + return fiatDisplay(fiatDisplayNumber, currentCurrency) } function fiatDisplay (fiatDisplayNumber, fiatSuffix) { diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js index e37aaa8c3..4a71e9585 100644 --- a/ui/app/components/hex-as-decimal-input.js +++ b/ui/app/components/hex-as-decimal-input.js @@ -139,7 +139,7 @@ HexAsDecimalInput.prototype.constructWarning = function () { } function hexify (decimalString) { - const hexBN = new BN(decimalString, 10) + const hexBN = new BN(parseInt(decimalString), 10) return '0x' + hexBN.toString('hex') } diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 1bb92301e..58bd2bdc4 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const isNode = require('detect-node') const findDOMNode = require('react-dom').findDOMNode const jazzicon = require('jazzicon') const iconFactoryGen = require('../../lib/icon-factory') @@ -34,19 +35,22 @@ IdenticonComponent.prototype.render = function () { IdenticonComponent.prototype.componentDidMount = function () { var props = this.props - const { address, network } = props + const { address } = props if (!address) return var container = findDOMNode(this) var diameter = props.diameter || this.defaultDiameter - var img = iconFactory.iconForAddress(address, diameter, false, network) - container.appendChild(img) + + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } } IdenticonComponent.prototype.componentDidUpdate = function () { var props = this.props - const { address, network } = props + const { address } = props if (!address) return @@ -58,6 +62,9 @@ IdenticonComponent.prototype.componentDidUpdate = function () { } var diameter = props.diameter || this.defaultDiameter - var img = iconFactory.iconForAddress(address, diameter, false, network) - container.appendChild(img) + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } } + diff --git a/ui/app/components/network.js b/ui/app/components/network.js index d9045167f..31a8fc17c 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -34,7 +34,7 @@ Network.prototype.render = function () { } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' - } else if (providerName === 'testnet') { + } else if (providerName === 'ropsten') { hoverText = 'Ropsten Test Network' iconName = 'ropsten-test-network' } else if (parseInt(networkNumber) === 3) { @@ -43,6 +43,9 @@ Network.prototype.render = function () { } else if (providerName === 'kovan') { hoverText = 'Kovan Test Network' iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' } else { hoverText = 'Unknown Private Network' iconName = 'unknown-private-network' @@ -82,6 +85,15 @@ Network.prototype.render = function () { }}, 'Kovan Test Net'), ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) default: return h('.network-indicator', [ h('i.fa.fa-question-circle.fa-lg', { diff --git a/ui/app/components/notice.js b/ui/app/components/notice.js index b85787033..d9f0067cd 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/notice.js @@ -107,7 +107,7 @@ Notice.prototype.render = function () { style: { marginTop: '18px', }, - }, 'Continue'), + }, 'Accept'), ]) ) } @@ -115,8 +115,9 @@ Notice.prototype.render = function () { Notice.prototype.componentDidMount = function () { var node = findDOMNode(this) linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { this.setState({disclaimerDisabled: false}) } - + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } } Notice.prototype.componentWillUnmount = function () { diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 1b83f5043..4b1a00eca 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -1,29 +1,26 @@ const Component = require('react').Component -const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits const actions = require('../actions') +const clone = require('clone') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN const hexToBn = require('../../../app/scripts/lib/hex-to-bn') - +const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') const EthBalance = require('./eth-balance') -const util = require('../util') const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') -const HexInput = require('./hex-as-decimal-input') +const BNInput = require('./bn-as-decimal-input') -const MIN_GAS_PRICE_BN = new BN(20000000) +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) const MIN_GAS_LIMIT_BN = new BN(21000) -module.exports = connect(mapStateToProps)(PendingTx) - -function mapStateToProps (state) { - return {} -} - +module.exports = PendingTx inherits(PendingTx, Component) function PendingTx () { Component.call(this) @@ -35,19 +32,29 @@ function PendingTx () { PendingTx.prototype.render = function () { const props = this.props + const { currentCurrency, blockGasLimit } = props + const conversionRate = props.conversionRate const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} + // Account Details const address = txParams.from || props.selectedAddress const identity = props.identities[address] || { address: address } const account = props.accounts[address] const balance = account ? account.balance : '0x0' - const gas = txParams.gas - const gasPrice = txParams.gasPrice + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + // Gas + const gas = txParams.gas const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) const gasPriceBn = hexToBn(gasPrice) const txFeeBn = gasBn.mul(gasPriceBn) @@ -55,7 +62,6 @@ PendingTx.prototype.render = function () { const maxCost = txFeeBn.add(valueBn) const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - const imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons const balanceBn = hexToBn(balance) const insufficientBalance = balanceBn.lt(maxCost) @@ -69,17 +75,8 @@ PendingTx.prototype.render = function () { }, [ h('form#pending-tx-form', { - onSubmit: (event) => { - event.preventDefault() - const form = document.querySelector('form#pending-tx-form') - const valid = form.checkValidity() - this.setState({ valid }) - if (valid && this.verifyGasParams()) { - props.sendTransaction(txMeta, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - } - }, + onSubmit: this.onSubmit.bind(this), + }, [ // tx info @@ -93,7 +90,6 @@ PendingTx.prototype.render = function () { h(MiniAccountPanel, { imageSeed: address, - imageifyIdenticons: imageify, picOrder: 'right', }, [ h('span.font-small', { @@ -101,11 +97,16 @@ PendingTx.prototype.render = function () { fontFamily: 'Montserrat Bold, Montserrat, sans-serif', }, }, identity.name), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), h('span.font-small', { style: { @@ -114,6 +115,8 @@ PendingTx.prototype.render = function () { }, [ h(EthBalance, { value: balance, + conversionRate, + currentCurrency, inline: true, labelColor: '#F7861C', }), @@ -151,7 +154,7 @@ PendingTx.prototype.render = function () { // in the way that gas and gasLimit currently are. h('.row', [ h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value }), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), ]), // Gas Limit (customizable) @@ -159,22 +162,21 @@ PendingTx.prototype.render = function () { h('.cell.label', 'Gas Limit'), h('.cell.value', { }, [ - h(HexInput, { + h(BNInput, { name: 'Gas Limit', - value: gas, + value: gasBn, + precision: 0, + scale: 0, // The hard lower limit for gas. min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, suffix: 'UNITS', style: { position: 'relative', top: '5px', }, - onChange: (newHex) => { - log.info(`Gas limit changed to ${newHex}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = newHex - this.setState({ txData: txMeta }) - }, + onChange: this.gasLimitChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, }), ]), @@ -185,21 +187,18 @@ PendingTx.prototype.render = function () { h('.cell.label', 'Gas Price'), h('.cell.value', { }, [ - h(HexInput, { + h(BNInput, { name: 'Gas Price', - value: gasPrice, - suffix: 'WEI', - min: MIN_GAS_PRICE_BN.toString(10), + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), style: { position: 'relative', top: '5px', }, - onChange: (newHex) => { - log.info(`Gas price changed to: ${newHex}`) - const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = newHex - this.setState({ txData: txMeta }) - }, + onChange: this.gasPriceChanged.bind(this), ref: (hexInput) => { this.inputs.push(hexInput) }, }), ]), @@ -208,7 +207,7 @@ PendingTx.prototype.render = function () { // Max Transaction Fee (calculated) h('.cell.row', [ h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFeeBn.toString(16) }), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), ]), h('.cell.row', { @@ -227,6 +226,8 @@ PendingTx.prototype.render = function () { }, [ h(EthBalance, { value: maxCost.toString(16), + currentCurrency, + conversionRate, inline: true, labelColor: 'black', fontSize: '16px', @@ -269,6 +270,15 @@ PendingTx.prototype.render = function () { }, 'Transaction Error. Exception thrown in contract code.') : null, + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + insufficientBalance ? h('span.error', { style: { @@ -289,7 +299,7 @@ PendingTx.prototype.render = function () { insufficientBalance ? - h('button', { + h('button.btn-green', { onClick: props.buyEth, }, 'Buy Ether') : null, @@ -306,7 +316,7 @@ PendingTx.prototype.render = function () { type: 'submit', value: 'ACCEPT', style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid, + disabled: insufficientBalance || !this.state.valid || !isValidAddress, }), h('button.cancel.btn-red', { @@ -323,29 +333,33 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { const txData = props.txData const txParams = txData.txParams || {} const isContractDeploy = !('to' in txParams) - const imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons // If it's not a contract deploy, send to the account if (!isContractDeploy) { return h(MiniAccountPanel, { imageSeed: txParams.to, - imageifyIdenticons: imageify, picOrder: 'left', }, [ + h('span.font-small', { style: { fontFamily: 'Montserrat Bold, Montserrat, sans-serif', }, }, nameForAddress(txParams.to, props.identities)), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + ]) } else { return h(MiniAccountPanel, { - imageifyIdenticons: imageify, picOrder: 'left', }, [ @@ -359,6 +373,26 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { } } +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + PendingTx.prototype.resetGasFields = function () { log.debug(`pending-tx resetGasFields`) @@ -374,12 +408,39 @@ PendingTx.prototype.resetGasFields = function () { }) } +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + // After a customizable state value has been updated, PendingTx.prototype.gatherTxMeta = function () { log.debug(`pending-tx gatherTxMeta`) const props = this.props const state = this.state - const txData = state.txData || props.txData + const txData = clone(state.txData) || clone(props.txData) log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) return txData @@ -398,9 +459,14 @@ PendingTx.prototype._notZeroOrEmptyString = function (obj) { return obj !== '' && obj !== '0x0' } +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + function forwardCarrat () { return ( - h('img', { src: 'images/forward-carrat.svg', style: { @@ -408,7 +474,5 @@ function forwardCarrat () { height: '37px', }, }) - ) } - diff --git a/ui/app/components/qr-code.js b/ui/app/components/qr-code.js index 5488599eb..06b9aed9b 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/qr-code.js @@ -3,6 +3,7 @@ const h = require('react-hyperscript') const qrCode = require('qrcode-npm').qrcode const inherits = require('util').inherits const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed const CopyButton = require('./copyButton') module.exports = connect(mapStateToProps)(QrCodeView) @@ -22,13 +23,12 @@ function QrCodeView () { } QrCodeView.prototype.render = function () { - var props = this.props - var Qr = props.Qr - var qrImage = qrCode(4, 'M') - - qrImage.addData(Qr.data) + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) qrImage.make() - return h('.main-container.flex-column', { key: 'qr', style: { diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 8c9686035..e0a720426 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -43,14 +43,18 @@ ShapeshiftForm.prototype.renderMain = function () { style: { // marginTop: '10px', padding: '25px', + paddingTop: '5px', width: '100%', + minHeight: '215px', alignItems: 'center', + overflowY: 'auto', }, }, [ h('.flex-row', { style: { justifyContent: 'center', alignItems: 'baseline', + height: '42px', }, }, [ h('img', { @@ -66,6 +70,7 @@ ShapeshiftForm.prototype.renderMain = function () { h('input#fromCoin.buy-inputs.ex-coins', { type: 'text', list: 'coinList', + autoFocus: true, dataset: { persistentFormId: 'input-coin', }, @@ -92,7 +97,6 @@ ShapeshiftForm.prototype.renderMain = function () { h('.icon-control', [ h('i.fa.fa-refresh.fa-4.orange', { style: { - position: 'relative', bottom: '5px', left: '5px', color: '#F7861C', @@ -121,8 +125,6 @@ ShapeshiftForm.prototype.renderMain = function () { }, }), ]), - - this.props.isSubLoading ? this.renderLoading() : null, h('.flex-column', { style: { alignItems: 'flex-start', @@ -138,17 +140,6 @@ ShapeshiftForm.prototype.renderMain = function () { this.props.warning) : this.renderInfo(), ]), - h('.flex-row', { - style: { - padding: '10px', - paddingBottom: '2px', - width: '100%', - }, - }, [ - h('div', 'Receiving address:'), - h('.ellip-address', this.props.buyView.buyAddress), - ]), - h(this.activeToggle('.input-container'), { style: { padding: '10px', @@ -156,6 +147,7 @@ ShapeshiftForm.prototype.renderMain = function () { width: '100%', }, }, [ + h('div', `${coin} Address:`), h('input#fromCoinAddress.buy-inputs', { @@ -166,8 +158,8 @@ ShapeshiftForm.prototype.renderMain = function () { }, style: { boxSizing: 'border-box', - width: '278px', - height: '20px', + width: '227px', + height: '30px', padding: ' 5px ', }, }), @@ -177,7 +169,7 @@ ShapeshiftForm.prototype.renderMain = function () { fontSize: '12px', color: '#F7861C', position: 'relative', - bottom: '5px', + bottom: '10px', right: '11px', }, }), @@ -190,6 +182,8 @@ ShapeshiftForm.prototype.renderMain = function () { onClick: this.shift.bind(this), style: { marginTop: '10px', + position: 'relative', + bottom: '40px', }, }, 'Submit'), @@ -266,8 +260,6 @@ ShapeshiftForm.prototype.renderInfo = function () { return h('span', { style: { - marginTop: '10px', - marginBottom: '15px', }, }, [ h('h3.flex-row.text-transform-uppercase', { @@ -286,10 +278,6 @@ ShapeshiftForm.prototype.renderInfo = function () { ]) } -ShapeshiftForm.prototype.handleAddress = function (event) { - this.props.dispatch(actions.updateBuyAddress(event.target.value)) -} - ShapeshiftForm.prototype.activeToggle = function (elementType) { if (!this.props.buyView.formView.response || this.props.warning) return elementType return `${elementType}.inactive` diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 96a7cba6e..32bfbeda4 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -8,14 +8,17 @@ const actions = require('../actions') const addressSummary = require('../util').addressSummary const CopyButton = require('./copyButton') -const EtherBalance = require('./eth-balance') +const EthBalance = require('./eth-balance') const Tooltip = require('./tooltip') module.exports = connect(mapStateToProps)(ShiftListItem) function mapStateToProps (state) { - return {} + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } } inherits(ShiftListItem, Component) @@ -64,6 +67,7 @@ function formatDate (date) { ShiftListItem.prototype.renderUtilComponents = function () { var props = this.props + const { conversionRate, currentCurrency } = props switch (props.response.status) { case 'no_deposits': @@ -94,8 +98,10 @@ ShiftListItem.prototype.renderUtilComponents = function () { h(CopyButton, { value: this.props.response.transaction, }), - h(EtherBalance, { + h(EthBalance, { value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, width: '55px', shorten: true, needsParse: false, diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js index ca2781451..431054340 100644 --- a/ui/app/components/transaction-list-item-icon.js +++ b/ui/app/components/transaction-list-item-icon.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const Tooltip = require('./tooltip') const Identicon = require('./identicon') @@ -15,7 +16,7 @@ TransactionIcon.prototype.render = function () { const { transaction, txParams, isMsg } = this.props switch (transaction.status) { case 'unapproved': - return h( !isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') case 'rejected': return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { @@ -32,11 +33,16 @@ TransactionIcon.prototype.render = function () { }) case 'submitted': - return h('i.fa.fa-ellipsis-h', { - style: { - fontSize: '27px', - }, - }) + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) } if (isMsg) { diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index 9fef52355..dbda66a31 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -2,12 +2,13 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const EtherBalance = require('./eth-balance') +const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary const explorerLink = require('../../lib/explorer-link') const CopyButton = require('./copyButton') const vreme = new (require('vreme')) const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') const TransactionIcon = require('./transaction-list-item-icon') const ShiftListItem = require('./shift-list-item') @@ -19,7 +20,7 @@ function TransactionListItem () { } TransactionListItem.prototype.render = function () { - const { transaction, network } = this.props + const { transaction, network, conversionRate, currentCurrency } = this.props if (transaction.key === 'shapeshift') { if (network === '1') return h(ShiftListItem, transaction) } @@ -27,7 +28,7 @@ TransactionListItem.prototype.render = function () { let isLinkable = false const numericNet = parseInt(network) - isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 42 + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 var isMsg = ('msgParams' in transaction) var isTx = ('txParams' in transaction) @@ -39,6 +40,8 @@ TransactionListItem.prototype.render = function () { txParams = transaction.msgParams } + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + const isClickable = ('hash' in transaction && isLinkable) || isPending return ( h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { @@ -69,6 +72,22 @@ TransactionListItem.prototype.render = function () { ]), ]), + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ domainField(txParams), h('div', date), @@ -78,8 +97,10 @@ TransactionListItem.prototype.render = function () { // Places a copy button if tx is successful, else places a placeholder empty div. transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), - isTx ? h(EtherBalance, { + isTx ? h(EthBalance, { value: txParams.value, + conversionRate, + currentCurrency, width: '55px', shorten: true, showFiat: false, @@ -134,7 +155,6 @@ function failIfFailed (transaction) { return h('span.error', ' (Rejected)') } if (transaction.err) { - return h(Tooltip, { title: transaction.err.message, position: 'bottom', @@ -142,5 +162,4 @@ function failIfFailed (transaction) { h('span.error', ' (Failed)'), ]) } - } diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js index 4c25f3dd9..3b4ba741e 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/app/components/transaction-list.js @@ -13,7 +13,7 @@ function TransactionList () { } TransactionList.prototype.render = function () { - const { transactions, network, unapprovedMsgs } = this.props + const { transactions, network, unapprovedMsgs, conversionRate } = this.props var shapeShiftTxList if (network === '1') { @@ -58,6 +58,7 @@ TransactionList.prototype.render = function () { } return h(TransactionListItem, { transaction, i, network, key, + conversionRate, showTx: (txId) => { this.props.viewPendingTx(txId) }, |