aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/app/components/bn-as-decimal-input.js174
-rw-r--r--ui/app/components/copyable.js46
-rw-r--r--ui/app/components/ens-input.js4
-rw-r--r--ui/app/components/hex-as-decimal-input.js2
-rw-r--r--ui/app/components/pending-tx.js95
-rw-r--r--ui/app/components/transaction-list-item-icon.js16
-rw-r--r--ui/app/components/transaction-list-item.js19
-rw-r--r--ui/app/conf-tx.js3
-rw-r--r--ui/app/util.js1
-rw-r--r--ui/lib/contract-namer.js12
-rw-r--r--ui/lib/icon-factory.js40
11 files changed, 352 insertions, 60 deletions
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/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/ens-input.js b/ui/app/components/ens-input.js
index 3e44d83af..43bb7ab22 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -21,6 +21,7 @@ EnsInput.prototype.render = function () {
const opts = extend(props, {
list: 'addresses',
onChange: () => {
+ this.setState({ ensResolution: '0x0000000000000000000000000000000000000000' })
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
if (!networkHasEnsSupport) return
@@ -95,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,
})
}
})
@@ -108,6 +111,7 @@ EnsInput.prototype.lookupEnsName = function () {
log.error(reason)
return this.setState({
loadingEns: false,
+ ensResolution: '0x0000000000000000000000000000000000000000',
ensFailure: true,
hoverText: reason.message,
})
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/pending-tx.js b/ui/app/components/pending-tx.js
index 0d1f06ba6..e3b307b0b 100644
--- a/ui/app/components/pending-tx.js
+++ b/ui/app/components/pending-tx.js
@@ -2,17 +2,18 @@ const Component = require('react').Component
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_GWEI_BN = new BN(2)
const GWEI_FACTOR = new BN(1e9)
@@ -43,14 +44,17 @@ PendingTx.prototype.render = function () {
const account = props.accounts[address]
const balance = account ? account.balance : '0x0'
+ // recipient check
+ const isValidAddress = !txParams.to || util.isValidAddress(txParams.to)
+
// Gas
const gas = txParams.gas
const gasBn = hexToBn(gas)
+ const safeGasLimit = parseInt(txMeta.blockGasLimit)
// Gas Price
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
const gasPriceBn = hexToBn(gasPrice)
- const gasPriceGweiBn = gasPriceBn.div(GWEI_FACTOR)
const txFeeBn = gasBn.mul(gasPriceBn)
const valueBn = hexToBn(txParams.value)
@@ -92,11 +96,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: {
@@ -152,11 +161,14 @@ 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',
@@ -174,9 +186,11 @@ PendingTx.prototype.render = function () {
h('.cell.label', 'Gas Price'),
h('.cell.value', {
}, [
- h(HexInput, {
+ h(BNInput, {
name: 'Gas Price',
- value: gasPriceGweiBn.toString(16),
+ value: gasPriceBn,
+ precision: 9,
+ scale: 9,
suffix: 'GWEI',
min: MIN_GAS_PRICE_GWEI_BN.toString(10),
style: {
@@ -255,6 +269,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: {
@@ -292,7 +315,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', {
@@ -316,16 +339,23 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () {
imageSeed: txParams.to,
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, {
@@ -342,19 +372,24 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () {
}
}
-PendingTx.prototype.gasPriceChanged = function (newHex) {
- log.info(`Gas price changed to: ${newHex}`)
- const inWei = hexToBn(newHex).mul(GWEI_FACTOR)
+PendingTx.prototype.gasPriceChanged = function (newBN, valid) {
+ log.info(`Gas price changed to: ${newBN.toString(10)}`)
const txMeta = this.gatherTxMeta()
- txMeta.txParams.gasPrice = inWei.toString(16)
- this.setState({ txData: txMeta })
+ txMeta.txParams.gasPrice = '0x' + newBN.toString('hex')
+ this.setState({
+ txData: clone(txMeta),
+ valid,
+ })
}
-PendingTx.prototype.gasLimitChanged = function (newHex) {
- log.info(`Gas limit changed to ${newHex}`)
+PendingTx.prototype.gasLimitChanged = function (newBN, valid) {
+ log.info(`Gas limit changed to ${newBN.toString(10)}`)
const txMeta = this.gatherTxMeta()
- txMeta.txParams.gas = newHex
- this.setState({ txData: txMeta })
+ txMeta.txParams.gas = '0x' + newBN.toString('hex')
+ this.setState({
+ txData: clone(txMeta),
+ valid,
+ })
}
PendingTx.prototype.resetGasFields = function () {
@@ -404,7 +439,7 @@ 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
@@ -425,7 +460,6 @@ PendingTx.prototype._notZeroOrEmptyString = function (obj) {
function forwardCarrat () {
return (
-
h('img', {
src: 'images/forward-carrat.svg',
style: {
@@ -433,6 +467,5 @@ function forwardCarrat () {
height: '37px',
},
})
-
)
}
diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js
index d63cae259..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')
@@ -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 c2a585003..dbda66a31 100644
--- a/ui/app/components/transaction-list-item.js
+++ b/ui/app/components/transaction-list-item.js
@@ -8,6 +8,7 @@ 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')
@@ -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),
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 0d7c4c1bb..4ae81f35f 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -48,6 +48,7 @@ ConfirmTxScreen.prototype.render = function () {
var txParams = txData.params || {}
var isNotification = isPopupOrNotification() === 'notification'
+
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
if (unconfTxList.length === 0) return h(Loading, { isLoading: true })
@@ -108,7 +109,7 @@ ConfirmTxScreen.prototype.render = function () {
currentCurrency,
// Actions
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
- sendTransaction: this.sendTransaction.bind(this, txData),
+ sendTransaction: this.sendTransaction.bind(this),
cancelTransaction: this.cancelTransaction.bind(this, txData),
signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData),
diff --git a/ui/app/util.js b/ui/app/util.js
index 7a56bf6a0..ac3f42c6b 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -61,6 +61,7 @@ function miniAddressSummary (address) {
function isValidAddress (address) {
var prefixed = ethUtil.addHexPrefix(address)
+ if (address === '0x0000000000000000000000000000000000000000') return false
return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
}
diff --git a/ui/lib/contract-namer.js b/ui/lib/contract-namer.js
index a94c62b62..f05e770cc 100644
--- a/ui/lib/contract-namer.js
+++ b/ui/lib/contract-namer.js
@@ -5,14 +5,18 @@
* otherwise returns null.
*/
-// Nickname keys must be stored in lower case.
-const nicknames = {}
+const contractMap = require('eth-contract-metadata')
+const ethUtil = require('ethereumjs-util')
module.exports = function (addr, identities = {}) {
+ const checksummed = ethUtil.toChecksumAddress(addr)
+ if (contractMap[checksummed] && contractMap[checksummed].name) {
+ return contractMap[checksummed].name
+ }
+
const address = addr.toLowerCase()
const ids = hashFromIdentities(identities)
-
- return addrFromHash(address, ids) || addrFromHash(address, nicknames)
+ return addrFromHash(address, ids)
}
function hashFromIdentities (identities) {
diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js
index 82cc839d6..45be47b7a 100644
--- a/ui/lib/icon-factory.js
+++ b/ui/lib/icon-factory.js
@@ -1,4 +1,7 @@
var iconFactory
+const isValidAddress = require('ethereumjs-util').isValidAddress
+const toChecksumAddress = require('ethereumjs-util').toChecksumAddress
+const contractMap = require('eth-contract-metadata')
module.exports = function (jazzicon) {
if (!iconFactory) {
@@ -12,22 +15,12 @@ function IconFactory (jazzicon) {
this.cache = {}
}
-IconFactory.prototype.iconForAddress = function (address, diameter, imageify) {
- if (imageify) {
- return this.generateIdenticonImg(address, diameter)
- } else {
- return this.generateIdenticonSvg(address, diameter)
+IconFactory.prototype.iconForAddress = function (address, diameter) {
+ const addr = toChecksumAddress(address)
+ if (iconExistsFor(addr)) {
+ return imageElFor(addr)
}
-}
-
-// returns img dom element
-IconFactory.prototype.generateIdenticonImg = function (address, diameter) {
- var identicon = this.generateIdenticonSvg(address, diameter)
- var identiconSrc = identicon.innerHTML
- var dataUri = toDataUri(identiconSrc)
- var img = document.createElement('img')
- img.src = dataUri
- return img
+ return this.generateIdenticonSvg(address, diameter)
}
// returns svg dom element
@@ -49,12 +42,23 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) {
// util
+function iconExistsFor (address) {
+ return (contractMap.address) && isValidAddress(address) && (contractMap[address].logo)
+}
+
+function imageElFor (address) {
+ const contract = contractMap[address]
+ const fileName = contract.logo
+ const path = `images/contract/${fileName}`
+ const img = document.createElement('img')
+ img.src = path
+ img.style.width = '100%'
+ return img
+}
+
function jsNumberForAddress (address) {
var addr = address.slice(2, 10)
var seed = parseInt(addr, 16)
return seed
}
-function toDataUri (identiconSrc) {
- return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc)
-}