aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.babelrc4
-rw-r--r--CHANGELOG.md3
-rw-r--r--app/scripts/controllers/infura.js42
-rw-r--r--app/scripts/controllers/preferences.js8
-rw-r--r--app/scripts/controllers/transactions.js1
-rw-r--r--app/scripts/keyring-controller.js2
-rw-r--r--app/scripts/metamask-controller.js16
-rw-r--r--circle.yml2
-rw-r--r--gulpfile.js16
-rw-r--r--package.json8
-rw-r--r--test/integration/index.js14
-rw-r--r--test/unit/infura-controller-test.js34
-rw-r--r--ui/app/account-detail.js40
-rw-r--r--ui/app/actions.js25
-rw-r--r--ui/app/app.js21
-rw-r--r--ui/app/components/account-export.js2
-rw-r--r--ui/app/components/balance.js89
-rw-r--r--ui/app/components/identicon.js14
-rw-r--r--ui/app/components/loading.js5
-rw-r--r--ui/app/components/pending-tx.js6
-rw-r--r--ui/app/components/tab-bar.js1
-rw-r--r--ui/app/components/token-cell.js46
-rw-r--r--ui/app/components/token-list.js147
-rw-r--r--ui/app/components/transaction-list.js11
-rw-r--r--ui/app/info.js1
-rw-r--r--ui/lib/icon-factory.js5
26 files changed, 507 insertions, 56 deletions
diff --git a/.babelrc b/.babelrc
index 3ca197980..bca3364fc 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,4 +1,4 @@
{
- "presets": ["es2015"],
- "plugins": ["transform-runtime"]
+ "presets": ["es2015", "stage-0"],
+ "plugins": ["transform-runtime", "transform-async-to-generator"]
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8200c0d3c..96dc79d9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,13 @@
## Current Master
+- Add list of popular tokens held to the account detail view.
- Add a warning to JSON file import.
- Fix bug where slowly mined txs would sometimes be incorrectly marked as failed.
- Fix bug where badge count did not reflect personal_sign pending messages.
- Seed word confirmation wording is now scarier.
+- Fix error for invalid seed words.
+- Prevent users from submitting two duplicate transactions by disabling submit.
## 3.7.8 2017-6-12
diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js
new file mode 100644
index 000000000..98375b446
--- /dev/null
+++ b/app/scripts/controllers/infura.js
@@ -0,0 +1,42 @@
+const ObservableStore = require('obs-store')
+const extend = require('xtend')
+
+// every ten minutes
+const POLLING_INTERVAL = 300000
+
+class InfuraController {
+
+ constructor (opts = {}) {
+ const initState = extend({
+ infuraNetworkStatus: {},
+ }, opts.initState)
+ this.store = new ObservableStore(initState)
+ }
+
+ //
+ // PUBLIC METHODS
+ //
+
+ // Responsible for retrieving the status of Infura's nodes. Can return either
+ // ok, degraded, or down.
+ checkInfuraNetworkStatus () {
+ return fetch('https://api.infura.io/v1/status/metamask')
+ .then(response => response.json())
+ .then((parsedResponse) => {
+ this.store.updateState({
+ infuraNetworkStatus: parsedResponse,
+ })
+ })
+ }
+
+ scheduleInfuraNetworkCheck () {
+ if (this.conversionInterval) {
+ clearInterval(this.conversionInterval)
+ }
+ this.conversionInterval = setInterval(() => {
+ this.checkInfuraNetworkStatus()
+ }, POLLING_INTERVAL)
+ }
+}
+
+module.exports = InfuraController
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 7212c7c43..aa8e05fcc 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -7,6 +7,7 @@ class PreferencesController {
constructor (opts = {}) {
const initState = extend({
frequentRpcList: [],
+ currentAccountTab: 'history',
}, opts.initState)
this.store = new ObservableStore(initState)
}
@@ -35,6 +36,13 @@ class PreferencesController {
})
}
+ setCurrentAccountTab (currentAccountTab) {
+ return new Promise((resolve, reject) => {
+ this.store.updateState({ currentAccountTab })
+ resolve()
+ })
+ }
+
addToFrequentRpcList (_url) {
const rpcList = this.getFrequentRpcList()
const index = rpcList.findIndex((element) => { return element === _url })
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index 59c8be8b7..651565519 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -384,6 +384,7 @@ module.exports = class TransactionController extends EventEmitter {
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
+ // - `'failed'` the tx failed for some reason, included on tx data.
_setTxStatus (txId, status) {
var txMeta = this.getTx(txId)
txMeta.status = status
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index 5b3c80e40..2edc8060e 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -87,7 +87,7 @@ class KeyringController extends EventEmitter {
}
if (!bip39.validateMnemonic(seed)) {
- return Promise.reject('Seed phrase is invalid.')
+ return Promise.reject(new Error('Seed phrase is invalid.'))
}
this.clearKeyrings()
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 755bf3289..be64a45c7 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -15,6 +15,7 @@ const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller')
const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book')
+const InfuraController = require('./controllers/infura')
const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager')
const TransactionController = require('./controllers/transactions')
@@ -44,8 +45,8 @@ module.exports = class MetamaskController extends EventEmitter {
this.store = new ObservableStore(initState)
// network store
-
this.networkController = new NetworkController(initState.NetworkController)
+
// config manager
this.configManager = new ConfigManager({
store: this.store,
@@ -63,6 +64,13 @@ module.exports = class MetamaskController extends EventEmitter {
this.currencyController.updateConversionRate()
this.currencyController.scheduleConversionInterval()
+ // infura controller
+ this.infuraController = new InfuraController({
+ initState: initState.InfuraController,
+ })
+ this.infuraController.scheduleInfuraNetworkCheck()
+
+
// rpc provider
this.provider = this.initializeProvider()
@@ -147,6 +155,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.store.subscribe((state) => {
this.store.updateState({ NetworkController: state })
})
+ this.infuraController.store.subscribe((state) => {
+ this.store.updateState({ InfuraController: state })
+ })
// manual mem state subscriptions
this.networkController.store.subscribe(this.sendUpdate.bind(this))
@@ -160,6 +171,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.currencyController.store.subscribe(this.sendUpdate.bind(this))
this.noticeController.memStore.subscribe(this.sendUpdate.bind(this))
this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this))
+ this.infuraController.store.subscribe(this.sendUpdate.bind(this))
}
//
@@ -237,6 +249,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.addressBookController.store.getState(),
this.currencyController.store.getState(),
this.noticeController.memStore.getState(),
+ this.infuraController.store.getState(),
// config manager
this.configManager.getConfig(),
this.shapeshiftController.store.getState(),
@@ -280,6 +293,7 @@ module.exports = class MetamaskController extends EventEmitter {
// PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
+ setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this),
diff --git a/circle.yml b/circle.yml
index 4305ca3b4..1f018ac24 100644
--- a/circle.yml
+++ b/circle.yml
@@ -1,6 +1,6 @@
machine:
node:
- version: 7.6.0
+ version: 8.0.0
dependencies:
pre:
- "npm i -g testem"
diff --git a/gulpfile.js b/gulpfile.js
index 5bba1b9b3..cc723704a 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -20,7 +20,7 @@ var gulpif = require('gulp-if')
var replace = require('gulp-replace')
var mkdirp = require('mkdirp')
-var disableLiveReload = gutil.env.disableLiveReload
+var disableDebugTools = gutil.env.disableDebugTools
var debug = gutil.env.debug
// browser reload
@@ -53,7 +53,7 @@ gulp.task('copy:images', copyTask({
],
}))
gulp.task('copy:contractImages', copyTask({
- source: './node_modules/ethereum-contract-icons/images/',
+ source: './node_modules/eth-contract-metadata/images/',
destinations: [
'./dist/firefox/images/contract',
'./dist/chrome/images/contract',
@@ -121,7 +121,7 @@ gulp.task('manifest:production', function() {
'./dist/chrome/manifest.json',
'./dist/edge/manifest.json',
],{base: './dist/'})
- .pipe(gulpif(disableLiveReload,jsoneditor(function(json) {
+ .pipe(gulpif(!debug,jsoneditor(function(json) {
json.background.scripts = ["scripts/background.js"]
return json
})))
@@ -138,7 +138,7 @@ const staticFiles = [
var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`)
copyStrings.push('copy:contractImages')
-if (!disableLiveReload) {
+if (debug) {
copyStrings.push('copy:reload')
}
@@ -234,7 +234,7 @@ function copyTask(opts){
destinations.forEach(function(destination) {
stream = stream.pipe(gulp.dest(destination))
})
- stream.pipe(gulpif(!disableLiveReload,livereload()))
+ stream.pipe(gulpif(debug,livereload()))
return stream
}
@@ -314,16 +314,16 @@ function bundleTask(opts) {
.pipe(buffer())
// sourcemaps
// loads map from browserify file
- .pipe(sourcemaps.init({loadMaps: true}))
+ .pipe(gulpif(debug, sourcemaps.init({loadMaps: true})))
// writes .map file
- .pipe(sourcemaps.write('./'))
+ .pipe(gulpif(debug, sourcemaps.write('./')))
// write completed bundles
.pipe(gulp.dest('./dist/firefox/scripts'))
.pipe(gulp.dest('./dist/chrome/scripts'))
.pipe(gulp.dest('./dist/edge/scripts'))
.pipe(gulp.dest('./dist/opera/scripts'))
// finally, trigger live reload
- .pipe(gulpif(!disableLiveReload, livereload()))
+ .pipe(gulpif(debug, livereload()))
)
}
diff --git a/package.json b/package.json
index 9085e4565..82792351d 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"start": "npm run dev",
"dev": "gulp dev --debug",
"disc": "gulp disc --debug",
- "dist": "npm install && gulp dist --disableLiveReload",
+ "dist": "npm install && gulp dist",
"test": "npm run lint && npm run test-unit && npm run test-integration",
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"single-test": "METAMASK_ENV=test mocha --require test/helper.js",
@@ -63,11 +63,12 @@
"end-of-stream": "^1.1.0",
"ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.1",
- "eth-contract-metadata": "^1.0.0",
+ "eth-contract-metadata": "^1.1.3",
"eth-hd-keyring": "^1.1.1",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.1.1",
"eth-simple-keyring": "^1.1.1",
+ "eth-token-tracker": "^1.0.9",
"ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0",
@@ -129,8 +130,11 @@
"xtend": "^4.0.1"
},
"devDependencies": {
+ "babel-core": "^6.24.1",
"babel-eslint": "^6.0.5",
+ "babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
+ "babel-polyfill": "^6.23.0",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.7.2",
"babelify": "^7.2.0",
diff --git a/test/integration/index.js b/test/integration/index.js
index f2d656b0b..85f91d92b 100644
--- a/test/integration/index.js
+++ b/test/integration/index.js
@@ -9,13 +9,15 @@ var b = browserify()
// Remove old bundle
try {
fs.unlinkSync(bundlePath)
-} catch (e) {}
-var writeStream = fs.createWriteStream(bundlePath)
+ var writeStream = fs.createWriteStream(bundlePath)
-tests.forEach(function (fileName) {
- b.add(path.join(__dirname, 'lib', fileName))
-})
+ tests.forEach(function (fileName) {
+ b.add(path.join(__dirname, 'lib', fileName))
+ })
-b.bundle().pipe(writeStream)
+ b.bundle().pipe(writeStream)
+} catch (e) {
+ console.error('Integration build failure', e)
+}
diff --git a/test/unit/infura-controller-test.js b/test/unit/infura-controller-test.js
new file mode 100644
index 000000000..7a2a114f9
--- /dev/null
+++ b/test/unit/infura-controller-test.js
@@ -0,0 +1,34 @@
+// polyfill fetch
+global.fetch = function () {return Promise.resolve({
+ json: () => { return Promise.resolve({"mainnet": "ok", "ropsten": "degraded", "kovan": "down", "rinkeby": "ok"}) },
+ })
+}
+const assert = require('assert')
+const InfuraController = require('../../app/scripts/controllers/infura')
+
+describe('infura-controller', function () {
+ var infuraController
+
+ beforeEach(function () {
+ infuraController = new InfuraController()
+ })
+
+ describe('network status queries', function () {
+ describe('#checkInfuraNetworkStatus', function () {
+ it('should return an object reflecting the network statuses', function (done) {
+ this.timeout(15000)
+ infuraController.checkInfuraNetworkStatus()
+ .then(() => {
+ const networkStatus = infuraController.store.getState().infuraNetworkStatus
+ assert.equal(Object.keys(networkStatus).length, 4)
+ assert.equal(networkStatus.mainnet, 'ok')
+ assert.equal(networkStatus.ropsten, 'degraded')
+ assert.equal(networkStatus.kovan, 'down')
+ })
+ .then(() => done())
+ .catch(done)
+
+ })
+ })
+ })
+})
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
index 7a78a360c..836032b3c 100644
--- a/ui/app/account-detail.js
+++ b/ui/app/account-detail.js
@@ -16,6 +16,9 @@ const ExportAccountView = require('./components/account-export')
const ethUtil = require('ethereumjs-util')
const EditableLabel = require('./components/editable-label')
const Tooltip = require('./components/tooltip')
+const TabBar = require('./components/tab-bar')
+const TokenList = require('./components/token-list')
+
module.exports = connect(mapStateToProps)(AccountDetailScreen)
function mapStateToProps (state) {
@@ -31,6 +34,7 @@ function mapStateToProps (state) {
transactions: state.metamask.selectedAddressTxList || [],
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
+ currentAccountTab: state.metamask.currentAccountTab,
}
}
@@ -237,11 +241,44 @@ AccountDetailScreen.prototype.subview = function () {
switch (subview) {
case 'transactions':
- return this.transactionList()
+ return this.tabSections()
case 'export':
var state = extend({key: 'export'}, this.props)
return h(ExportAccountView, state)
default:
+ return this.tabSections()
+ }
+}
+
+AccountDetailScreen.prototype.tabSections = function () {
+ const { currentAccountTab } = this.props
+
+ return h('section.tabSection', [
+
+ h(TabBar, {
+ tabs: [
+ { content: 'Sent', key: 'history' },
+ { content: 'Tokens', key: 'tokens' },
+ ],
+ defaultTab: currentAccountTab || 'history',
+ tabSelected: (key) => {
+ this.props.dispatch(actions.setCurrentAccountTab(key))
+ },
+ }),
+
+ this.tabSwitchView(),
+ ])
+}
+
+AccountDetailScreen.prototype.tabSwitchView = function () {
+ const props = this.props
+ const { address, network } = props
+ const { currentAccountTab } = this.props
+
+ switch (currentAccountTab) {
+ case 'tokens':
+ return h(TokenList, { userAddress: address, network })
+ default:
return this.transactionList()
}
}
@@ -249,6 +286,7 @@ AccountDetailScreen.prototype.subview = function () {
AccountDetailScreen.prototype.transactionList = function () {
const {transactions, unapprovedMsgs, address,
network, shapeShiftTxList, conversionRate } = this.props
+
return h(TransactionList, {
transactions: transactions.sort((a, b) => b.time - a.time),
network,
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 1a3557cb4..b6b5d6eb1 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -74,6 +74,7 @@ var actions = {
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
setCurrentCurrency: setCurrentCurrency,
+ setCurrentAccountTab,
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
@@ -218,7 +219,7 @@ function confirmSeedWords () {
return dispatch(actions.displayWarning(err.message))
}
- console.log('Seed word cache cleared. ' + account)
+ log.info('Seed word cache cleared. ' + account)
dispatch(actions.showAccountDetail(account))
})
}
@@ -338,7 +339,7 @@ function setCurrentCurrency (currencyCode) {
background.setCurrentCurrency(currencyCode, (err, data) => {
dispatch(this.hideLoadingIndication())
if (err) {
- console.error(err.stack)
+ log.error(err.stack)
return dispatch(actions.displayWarning(err.message))
}
dispatch({
@@ -409,7 +410,7 @@ function sendTx (txData) {
background.approveTransaction(txData.id, (err) => {
if (err) {
dispatch(actions.txError(err))
- return console.error(err.message)
+ return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))
})
@@ -424,7 +425,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.txError(err))
- return console.error(err.message)
+ return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))
})
@@ -558,6 +559,11 @@ function lockMetamask () {
return callBackgroundThenUpdate(background.setLocked)
}
+function setCurrentAccountTab (newTabName) {
+ log.debug(`background.setCurrentAccountTab: ${newTabName}`)
+ return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName)
+}
+
function showAccountDetail (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@@ -965,6 +971,17 @@ function shapeShiftRequest (query, options, cb) {
// We hide loading indication.
// If it errored, we show a warning.
// If it didn't, we update the state.
+function callBackgroundThenUpdateNoSpinner (method, ...args) {
+ return (dispatch) => {
+ method.call(background, ...args, (err) => {
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ forceUpdateMetamaskState(dispatch)
+ })
+ }
+}
+
function callBackgroundThenUpdate (method, ...args) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
diff --git a/ui/app/app.js b/ui/app/app.js
index 53dbc3354..d444a8349 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -21,7 +21,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
const ConfigScreen = require('./config')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
-const LoadingIndicator = require('./components/loading')
+const Loading = require('./components/loading')
const SandwichExpando = require('sandwich-expando')
const MenuDroppo = require('menu-droppo')
const DropMenuItem = require('./components/drop-menu-item')
@@ -64,7 +64,11 @@ function mapStateToProps (state) {
App.prototype.render = function () {
var props = this.props
- const { isLoading, loadingMessage, transForward } = props
+ const { isLoading, loadingMessage, transForward, network } = props
+ const isLoadingNetwork = network === 'loading'
+ const loadMessage = loadingMessage || isLoadingNetwork ?
+ 'Searching for Network' : null
+
log.debug('Main ui render function')
return (
@@ -77,13 +81,16 @@ App.prototype.render = function () {
},
}, [
- h(LoadingIndicator, { isLoading, loadingMessage }),
-
// app bar
this.renderAppBar(),
this.renderNetworkDropdown(),
this.renderDropdown(),
+ h(Loading, {
+ isLoading: isLoading || isLoadingNetwork,
+ loadingMessage: loadMessage,
+ }),
+
// panel content
h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), {
style: {
@@ -124,7 +131,7 @@ App.prototype.renderAppBar = function () {
background: props.isUnlocked ? 'white' : 'none',
height: '36px',
position: 'relative',
- zIndex: 10,
+ zIndex: 12,
},
}, [
@@ -221,7 +228,7 @@ App.prototype.renderNetworkDropdown = function () {
onClickOutside: (event) => {
this.setState({ isNetworkMenuOpen: !isOpen })
},
- zIndex: 1,
+ zIndex: 11,
style: {
position: 'absolute',
left: 0,
@@ -300,7 +307,7 @@ App.prototype.renderDropdown = function () {
return h(MenuDroppo, {
isOpen: isOpen,
- zIndex: 1,
+ zIndex: 11,
onClickOutside: (event) => {
this.setState({ isMainMenuOpen: !isOpen })
},
diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js
index 888196c5d..394d878f7 100644
--- a/ui/app/components/account-export.js
+++ b/ui/app/components/account-export.js
@@ -20,8 +20,6 @@ function mapStateToProps (state) {
}
ExportAccountView.prototype.render = function () {
- console.log('EXPORT VIEW')
- console.dir(this.props)
var state = this.props
var accountDetail = state.accountDetail
diff --git a/ui/app/components/balance.js b/ui/app/components/balance.js
new file mode 100644
index 000000000..57ca84564
--- /dev/null
+++ b/ui/app/components/balance.js
@@ -0,0 +1,89 @@
+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 Tooltip = require('./tooltip.js')
+const FiatValue = require('./fiat-value.js')
+
+module.exports = EthBalanceComponent
+
+inherits(EthBalanceComponent, Component)
+function EthBalanceComponent () {
+ Component.call(this)
+}
+
+EthBalanceComponent.prototype.render = function () {
+ var props = this.props
+ let { value } = props
+ var style = props.style
+ 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,
+ }, [
+ h('div', {
+ style: {
+ display: 'inline',
+ width: width,
+ },
+ }, this.renderBalance(value)),
+ ])
+
+ )
+}
+EthBalanceComponent.prototype.renderBalance = function (value) {
+ var props = this.props
+ if (value === 'None') return value
+ if (value === '...') return value
+ var balanceObj = generateBalanceObject(value, props.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) {
+ balance = balanceObj.shortBalance
+ } else {
+ balance = balanceObj.balance
+ }
+
+ var label = balanceObj.label
+
+ return (
+ h(Tooltip, {
+ position: 'bottom',
+ title: `${ethNumber} ${ethSuffix}`,
+ }, h('div.flex-column', [
+ h('.flex-row', {
+ style: {
+ alignItems: 'flex-end',
+ lineHeight: '13px',
+ fontFamily: 'Montserrat Light',
+ textRendering: 'geometricPrecision',
+ },
+ }, [
+ h('div', {
+ style: {
+ width: '100%',
+ textAlign: 'right',
+ },
+ }, this.props.incoming ? `+${balance}` : balance),
+ h('div', {
+ style: {
+ color: ' #AEAEAE',
+ fontSize: '12px',
+ marginLeft: '5px',
+ },
+ }, label),
+ ]),
+
+ showFiat ? h(FiatValue, { value: props.value }) : null,
+ ]))
+ )
+}
diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js
index 9de854b54..c754bc6ba 100644
--- a/ui/app/components/identicon.js
+++ b/ui/app/components/identicon.js
@@ -23,7 +23,9 @@ IdenticonComponent.prototype.render = function () {
h('div', {
key: 'identicon-' + this.props.address,
style: {
- display: 'inline-block',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
height: diameter,
width: diameter,
borderRadius: diameter / 2,
@@ -35,21 +37,22 @@ IdenticonComponent.prototype.render = function () {
IdenticonComponent.prototype.componentDidMount = function () {
var props = this.props
- var address = props.address
+ const { address } = props
if (!address) return
var container = findDOMNode(this)
+
var diameter = props.diameter || this.defaultDiameter
if (!isNode) {
- var img = iconFactory.iconForAddress(address, diameter, false)
+ var img = iconFactory.iconForAddress(address, diameter)
container.appendChild(img)
}
}
IdenticonComponent.prototype.componentDidUpdate = function () {
var props = this.props
- var address = props.address
+ const { address } = props
if (!address) return
@@ -62,7 +65,8 @@ IdenticonComponent.prototype.componentDidUpdate = function () {
var diameter = props.diameter || this.defaultDiameter
if (!isNode) {
- var img = iconFactory.iconForAddress(address, diameter, false)
+ var img = iconFactory.iconForAddress(address, diameter)
container.appendChild(img)
}
}
+
diff --git a/ui/app/components/loading.js b/ui/app/components/loading.js
index 88dc535df..87d6f5d20 100644
--- a/ui/app/components/loading.js
+++ b/ui/app/components/loading.js
@@ -26,18 +26,21 @@ LoadingIndicator.prototype.render = function () {
style: {
zIndex: 10,
position: 'absolute',
+ flexDirection: 'column',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
- background: 'rgba(255, 255, 255, 0.5)',
+ background: 'rgba(255, 255, 255, 0.8)',
},
}, [
h('img', {
src: 'images/loading.svg',
}),
+ h('br'),
+
showMessageIfAny(loadingMessage),
]) : null,
])
diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js
index 4b1a00eca..f33a5d948 100644
--- a/ui/app/components/pending-tx.js
+++ b/ui/app/components/pending-tx.js
@@ -27,6 +27,7 @@ function PendingTx () {
this.state = {
valid: true,
txData: null,
+ submitting: false,
}
}
@@ -316,7 +317,7 @@ PendingTx.prototype.render = function () {
type: 'submit',
value: 'ACCEPT',
style: { marginLeft: '10px' },
- disabled: insufficientBalance || !this.state.valid || !isValidAddress,
+ disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting,
}),
h('button.cancel.btn-red', {
@@ -412,11 +413,12 @@ PendingTx.prototype.onSubmit = function (event) {
event.preventDefault()
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
- this.setState({ valid })
+ this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) {
this.props.sendTransaction(txMeta, event)
} else {
this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
+ this.setState({ submitting: false })
}
}
diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js
index 65078e0a4..6295e7dd9 100644
--- a/ui/app/components/tab-bar.js
+++ b/ui/app/components/tab-bar.js
@@ -33,3 +33,4 @@ TabBar.prototype.render = function () {
}))
)
}
+
diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js
new file mode 100644
index 000000000..d3a895d36
--- /dev/null
+++ b/ui/app/components/token-cell.js
@@ -0,0 +1,46 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const Identicon = require('./identicon')
+
+module.exports = TokenCell
+
+inherits(TokenCell, Component)
+function TokenCell () {
+ Component.call(this)
+}
+
+TokenCell.prototype.render = function () {
+ const props = this.props
+ const { address, symbol, string, network, userAddress } = props
+
+ return (
+ h('li.token-cell', {
+ style: { cursor: network === '1' ? 'pointer' : 'default' },
+ onClick: (event) => {
+ const url = urlFor(address, userAddress, network)
+ if (url) {
+ navigateTo(url)
+ }
+ },
+ }, [
+
+ h(Identicon, {
+ diameter: 50,
+ address,
+ network,
+ }),
+
+ h('h3', `${string || 0} ${symbol}`),
+ ])
+ )
+}
+
+function navigateTo (url) {
+ global.platform.openWindow({ url })
+}
+
+function urlFor (tokenAddress, address, network) {
+ return `https://etherscan.io/token/${tokenAddress}?a=${address}`
+}
+
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
new file mode 100644
index 000000000..633d3ccfe
--- /dev/null
+++ b/ui/app/components/token-list.js
@@ -0,0 +1,147 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const TokenTracker = require('eth-token-tracker')
+const TokenCell = require('./token-cell.js')
+const contracts = require('eth-contract-metadata')
+
+const tokens = []
+for (const address in contracts) {
+ const contract = contracts[address]
+ if (contract.erc20) {
+ contract.address = address
+ tokens.push(contract)
+ }
+}
+
+module.exports = TokenList
+
+inherits(TokenList, Component)
+function TokenList () {
+ this.state = { tokens, isLoading: true, network: null }
+ Component.call(this)
+}
+
+TokenList.prototype.render = function () {
+ const state = this.state
+ const { tokens, isLoading, error } = state
+
+ const { userAddress } = this.props
+
+ if (isLoading) {
+ return this.message('Loading')
+ }
+
+ if (error) {
+ log.error(error)
+ return this.message('There was a problem loading your token balances.')
+ }
+
+ const network = this.props.network
+
+ const tokenViews = tokens.map((tokenData) => {
+ tokenData.network = network
+ tokenData.userAddress = userAddress
+ return h(TokenCell, tokenData)
+ })
+
+ return (
+ h('ol', {
+ style: {
+ height: '302px',
+ overflowY: 'auto',
+ },
+ }, [h('style', `
+
+ li.token-cell {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 10px;
+ }
+
+ li.token-cell > h3 {
+ margin-left: 12px;
+ }
+
+ li.token-cell:hover {
+ background: white;
+ cursor: pointer;
+ }
+
+ `)].concat(tokenViews.length ? tokenViews : this.message('No Tokens Found.')))
+ )
+}
+
+TokenList.prototype.message = function (body) {
+ return h('div', {
+ style: {
+ display: 'flex',
+ height: '250px',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ }, body)
+}
+
+TokenList.prototype.componentDidMount = function () {
+ this.createFreshTokenTracker()
+}
+
+TokenList.prototype.createFreshTokenTracker = function () {
+ if (this.tracker) {
+ // Clean up old trackers when refreshing:
+ this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
+ }
+
+ if (!global.ethereumProvider) return
+ const { userAddress } = this.props
+ this.tracker = new TokenTracker({
+ userAddress,
+ provider: global.ethereumProvider,
+ tokens: tokens,
+ pollingInterval: 8000,
+ })
+
+
+ // Set up listener instances for cleaning up
+ this.balanceUpdater = this.updateBalances.bind(this)
+ this.showError = (error) => {
+ this.setState({ error, isLoading: false })
+ }
+ this.tracker.on('update', this.balanceUpdater)
+ this.tracker.on('error', this.showError)
+
+ this.tracker.updateBalances()
+ .then(() => {
+ this.updateBalances(this.tracker.serialize())
+ })
+ .catch((reason) => {
+ log.error(`Problem updating balances`, reason)
+ this.setState({ isLoading: false })
+ })
+}
+
+TokenList.prototype.componentWillUpdate = function (nextProps) {
+ if (nextProps.network === 'loading') return
+ const oldNet = this.props.network
+ const newNet = nextProps.network
+
+ if (oldNet && newNet && newNet !== oldNet) {
+ this.setState({ isLoading: true })
+ this.createFreshTokenTracker()
+ }
+}
+
+TokenList.prototype.updateBalances = function (tokenData) {
+ const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000')
+ this.setState({ tokens: heldTokens, isLoading: false })
+}
+
+TokenList.prototype.componentWillUnmount = function () {
+ if (!this.tracker) return
+ this.tracker.stop()
+}
+
diff --git a/ui/app/components/transaction-list.js b/ui/app/components/transaction-list.js
index 37a757309..3b4ba741e 100644
--- a/ui/app/components/transaction-list.js
+++ b/ui/app/components/transaction-list.js
@@ -36,17 +36,6 @@ TransactionList.prototype.render = function () {
}
`),
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- paddingTop: '4px',
- paddingBottom: '4px',
- },
- }, [
- 'History',
- ]),
-
h('.tx-list', {
style: {
overflowY: 'auto',
diff --git a/ui/app/info.js b/ui/app/info.js
index 89cb7854d..e8470de97 100644
--- a/ui/app/info.js
+++ b/ui/app/info.js
@@ -151,3 +151,4 @@ InfoScreen.prototype.render = function () {
InfoScreen.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}
+
diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js
index 45be47b7a..27a74de66 100644
--- a/ui/lib/icon-factory.js
+++ b/ui/lib/icon-factory.js
@@ -20,6 +20,7 @@ IconFactory.prototype.iconForAddress = function (address, diameter) {
if (iconExistsFor(addr)) {
return imageElFor(addr)
}
+
return this.generateIdenticonSvg(address, diameter)
}
@@ -43,7 +44,7 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) {
// util
function iconExistsFor (address) {
- return (contractMap.address) && isValidAddress(address) && (contractMap[address].logo)
+ return contractMap[address] && isValidAddress(address) && contractMap[address].logo
}
function imageElFor (address) {
@@ -52,7 +53,7 @@ function imageElFor (address) {
const path = `images/contract/${fileName}`
const img = document.createElement('img')
img.src = path
- img.style.width = '100%'
+ img.style.width = '75%'
return img
}