Merge branch 'master' into i1473-dappDefaultGasPrice

This commit is contained in:
Thomas Huang 2017-06-27 10:32:28 -07:00
commit 1977417017
28 changed files with 514 additions and 64 deletions

View File

@ -1,4 +1,4 @@
{ {
"presets": ["es2015"], "presets": ["es2015", "stage-0"],
"plugins": ["transform-runtime"] "plugins": ["transform-runtime", "transform-async-to-generator"]
} }

View File

@ -2,8 +2,13 @@
## Current Master ## Current Master
- Add list of popular tokens held to the account detail view.
- Add a warning to JSON file import. - Add a warning to JSON file import.
- Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. - 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 ## 3.7.8 2017-6-12

View File

@ -116,13 +116,15 @@ function setupController (initState) {
updateBadge() updateBadge()
controller.txController.on('updateBadge', updateBadge) controller.txController.on('updateBadge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge) controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)
// plugin badge text // plugin badge text
function updateBadge () { function updateBadge () {
var label = '' var label = ''
var unapprovedTxCount = controller.txController.unapprovedTxCount var unapprovedTxCount = controller.txController.unapprovedTxCount
var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
var count = unapprovedTxCount + unapprovedMsgCount var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount
var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs
if (count) { if (count) {
label = String(count) label = String(count)
} }

View File

@ -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

View File

@ -7,6 +7,7 @@ class PreferencesController {
constructor (opts = {}) { constructor (opts = {}) {
const initState = extend({ const initState = extend({
frequentRpcList: [], frequentRpcList: [],
currentAccountTab: 'history',
}, opts.initState) }, opts.initState)
this.store = new ObservableStore(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) { addToFrequentRpcList (_url) {
const rpcList = this.getFrequentRpcList() const rpcList = this.getFrequentRpcList()
const index = rpcList.findIndex((element) => { return element === _url }) const index = rpcList.findIndex((element) => { return element === _url })

View File

@ -384,13 +384,13 @@ module.exports = class TransactionController extends EventEmitter {
// - `'signed'` the tx is signed // - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server // - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block. // - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
_setTxStatus (txId, status) { _setTxStatus (txId, status) {
var txMeta = this.getTx(txId) var txMeta = this.getTx(txId)
txMeta.status = status txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId) this.emit(`${txMeta.id}:${status}`, txId)
if (status === 'submitted' || status === 'rejected') { if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta) this.emit(`${txMeta.id}:finished`, txMeta)
} }
this.updateTx(txMeta) this.updateTx(txMeta)
this.emit('updateBadge') this.emit('updateBadge')

View File

@ -87,7 +87,7 @@ class KeyringController extends EventEmitter {
} }
if (!bip39.validateMnemonic(seed)) { if (!bip39.validateMnemonic(seed)) {
return Promise.reject('Seed phrase is invalid.') return Promise.reject(new Error('Seed phrase is invalid.'))
} }
this.clearKeyrings() this.clearKeyrings()

View File

@ -15,6 +15,7 @@ const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller') const NoticeController = require('./notice-controller')
const ShapeShiftController = require('./controllers/shapeshift') const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book') const AddressBookController = require('./controllers/address-book')
const InfuraController = require('./controllers/infura')
const MessageManager = require('./lib/message-manager') const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager') const PersonalMessageManager = require('./lib/personal-message-manager')
const TransactionController = require('./controllers/transactions') const TransactionController = require('./controllers/transactions')
@ -44,8 +45,8 @@ module.exports = class MetamaskController extends EventEmitter {
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
// network store // network store
this.networkController = new NetworkController(initState.NetworkController) this.networkController = new NetworkController(initState.NetworkController)
// config manager // config manager
this.configManager = new ConfigManager({ this.configManager = new ConfigManager({
store: this.store, store: this.store,
@ -63,6 +64,13 @@ module.exports = class MetamaskController extends EventEmitter {
this.currencyController.updateConversionRate() this.currencyController.updateConversionRate()
this.currencyController.scheduleConversionInterval() this.currencyController.scheduleConversionInterval()
// infura controller
this.infuraController = new InfuraController({
initState: initState.InfuraController,
})
this.infuraController.scheduleInfuraNetworkCheck()
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
@ -147,6 +155,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.store.subscribe((state) => { this.networkController.store.subscribe((state) => {
this.store.updateState({ NetworkController: state }) this.store.updateState({ NetworkController: state })
}) })
this.infuraController.store.subscribe((state) => {
this.store.updateState({ InfuraController: state })
})
// manual mem state subscriptions // manual mem state subscriptions
this.networkController.store.subscribe(this.sendUpdate.bind(this)) 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.currencyController.store.subscribe(this.sendUpdate.bind(this))
this.noticeController.memStore.subscribe(this.sendUpdate.bind(this)) this.noticeController.memStore.subscribe(this.sendUpdate.bind(this))
this.shapeshiftController.store.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.addressBookController.store.getState(),
this.currencyController.store.getState(), this.currencyController.store.getState(),
this.noticeController.memStore.getState(), this.noticeController.memStore.getState(),
this.infuraController.store.getState(),
// config manager // config manager
this.configManager.getConfig(), this.configManager.getConfig(),
this.shapeshiftController.store.getState(), this.shapeshiftController.store.getState(),
@ -280,6 +293,7 @@ module.exports = class MetamaskController extends EventEmitter {
// PreferencesController // PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this),

View File

@ -1,6 +1,6 @@
machine: machine:
node: node:
version: 7.6.0 version: 8.0.0
dependencies: dependencies:
pre: pre:
- "npm i -g testem" - "npm i -g testem"

View File

@ -20,7 +20,7 @@ var gulpif = require('gulp-if')
var replace = require('gulp-replace') var replace = require('gulp-replace')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var disableLiveReload = gutil.env.disableLiveReload var disableDebugTools = gutil.env.disableDebugTools
var debug = gutil.env.debug var debug = gutil.env.debug
// browser reload // browser reload
@ -53,7 +53,7 @@ gulp.task('copy:images', copyTask({
], ],
})) }))
gulp.task('copy:contractImages', copyTask({ gulp.task('copy:contractImages', copyTask({
source: './node_modules/ethereum-contract-icons/images/', source: './node_modules/eth-contract-metadata/images/',
destinations: [ destinations: [
'./dist/firefox/images/contract', './dist/firefox/images/contract',
'./dist/chrome/images/contract', './dist/chrome/images/contract',
@ -121,7 +121,7 @@ gulp.task('manifest:production', function() {
'./dist/chrome/manifest.json', './dist/chrome/manifest.json',
'./dist/edge/manifest.json', './dist/edge/manifest.json',
],{base: './dist/'}) ],{base: './dist/'})
.pipe(gulpif(disableLiveReload,jsoneditor(function(json) { .pipe(gulpif(!debug,jsoneditor(function(json) {
json.background.scripts = ["scripts/background.js"] json.background.scripts = ["scripts/background.js"]
return json return json
}))) })))
@ -138,7 +138,7 @@ const staticFiles = [
var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`) var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`)
copyStrings.push('copy:contractImages') copyStrings.push('copy:contractImages')
if (!disableLiveReload) { if (debug) {
copyStrings.push('copy:reload') copyStrings.push('copy:reload')
} }
@ -234,7 +234,7 @@ function copyTask(opts){
destinations.forEach(function(destination) { destinations.forEach(function(destination) {
stream = stream.pipe(gulp.dest(destination)) stream = stream.pipe(gulp.dest(destination))
}) })
stream.pipe(gulpif(!disableLiveReload,livereload())) stream.pipe(gulpif(debug,livereload()))
return stream return stream
} }
@ -314,16 +314,16 @@ function bundleTask(opts) {
.pipe(buffer()) .pipe(buffer())
// sourcemaps // sourcemaps
// loads map from browserify file // loads map from browserify file
.pipe(sourcemaps.init({loadMaps: true})) .pipe(gulpif(debug, sourcemaps.init({loadMaps: true})))
// writes .map file // writes .map file
.pipe(sourcemaps.write('./')) .pipe(gulpif(debug, sourcemaps.write('./')))
// write completed bundles // write completed bundles
.pipe(gulp.dest('./dist/firefox/scripts')) .pipe(gulp.dest('./dist/firefox/scripts'))
.pipe(gulp.dest('./dist/chrome/scripts')) .pipe(gulp.dest('./dist/chrome/scripts'))
.pipe(gulp.dest('./dist/edge/scripts')) .pipe(gulp.dest('./dist/edge/scripts'))
.pipe(gulp.dest('./dist/opera/scripts')) .pipe(gulp.dest('./dist/opera/scripts'))
// finally, trigger live reload // finally, trigger live reload
.pipe(gulpif(!disableLiveReload, livereload())) .pipe(gulpif(debug, livereload()))
) )
} }

View File

@ -7,7 +7,7 @@
"start": "npm run dev", "start": "npm run dev",
"dev": "gulp dev --debug", "dev": "gulp dev --debug",
"disc": "gulp disc --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": "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\"", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
@ -62,11 +62,12 @@
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
"ensnare": "^1.0.0", "ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.1", "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-hd-keyring": "^1.1.1",
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.1.1", "eth-sig-util": "^1.1.1",
"eth-simple-keyring": "^1.1.1", "eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.0.9",
"ethereumjs-tx": "^1.3.0", "ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
@ -128,8 +129,11 @@
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.24.1",
"babel-eslint": "^6.0.5", "babel-eslint": "^6.0.5",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-stage-0": "^6.24.1", "babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.7.2", "babel-register": "^6.7.2",
"babelify": "^7.2.0", "babelify": "^7.2.0",

View File

@ -9,13 +9,15 @@ var b = browserify()
// Remove old bundle // Remove old bundle
try { try {
fs.unlinkSync(bundlePath) fs.unlinkSync(bundlePath)
} catch (e) {}
var writeStream = fs.createWriteStream(bundlePath) var writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function (fileName) { tests.forEach(function (fileName) {
b.add(path.join(__dirname, 'lib', fileName)) b.add(path.join(__dirname, 'lib', fileName))
}) })
b.bundle().pipe(writeStream) b.bundle().pipe(writeStream)
} catch (e) {
console.error('Integration build failure', e)
}

View File

@ -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)
})
})
})
})

View File

@ -16,6 +16,9 @@ const ExportAccountView = require('./components/account-export')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EditableLabel = require('./components/editable-label') const EditableLabel = require('./components/editable-label')
const Tooltip = require('./components/tooltip') const Tooltip = require('./components/tooltip')
const TabBar = require('./components/tab-bar')
const TokenList = require('./components/token-list')
module.exports = connect(mapStateToProps)(AccountDetailScreen) module.exports = connect(mapStateToProps)(AccountDetailScreen)
function mapStateToProps (state) { function mapStateToProps (state) {
@ -31,6 +34,7 @@ function mapStateToProps (state) {
transactions: state.metamask.selectedAddressTxList || [], transactions: state.metamask.selectedAddressTxList || [],
conversionRate: state.metamask.conversionRate, conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency, currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab,
} }
} }
@ -237,10 +241,43 @@ AccountDetailScreen.prototype.subview = function () {
switch (subview) { switch (subview) {
case 'transactions': case 'transactions':
return this.transactionList() return this.tabSections()
case 'export': case 'export':
var state = extend({key: 'export'}, this.props) var state = extend({key: 'export'}, this.props)
return h(ExportAccountView, state) 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: default:
return this.transactionList() return this.transactionList()
} }
@ -249,6 +286,7 @@ AccountDetailScreen.prototype.subview = function () {
AccountDetailScreen.prototype.transactionList = function () { AccountDetailScreen.prototype.transactionList = function () {
const {transactions, unapprovedMsgs, address, const {transactions, unapprovedMsgs, address,
network, shapeShiftTxList, conversionRate } = this.props network, shapeShiftTxList, conversionRate } = this.props
return h(TransactionList, { return h(TransactionList, {
transactions: transactions.sort((a, b) => b.time - a.time), transactions: transactions.sort((a, b) => b.time - a.time),
network, network,

View File

@ -74,6 +74,7 @@ var actions = {
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
setCurrentCurrency: setCurrentCurrency, setCurrentCurrency: setCurrentCurrency,
setCurrentAccountTab,
// account detail screen // account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage, showSendPage: showSendPage,
@ -218,7 +219,7 @@ function confirmSeedWords () {
return dispatch(actions.displayWarning(err.message)) return dispatch(actions.displayWarning(err.message))
} }
console.log('Seed word cache cleared. ' + account) log.info('Seed word cache cleared. ' + account)
dispatch(actions.showAccountDetail(account)) dispatch(actions.showAccountDetail(account))
}) })
} }
@ -338,7 +339,7 @@ function setCurrentCurrency (currencyCode) {
background.setCurrentCurrency(currencyCode, (err, data) => { background.setCurrentCurrency(currencyCode, (err, data) => {
dispatch(this.hideLoadingIndication()) dispatch(this.hideLoadingIndication())
if (err) { if (err) {
console.error(err.stack) log.error(err.stack)
return dispatch(actions.displayWarning(err.message)) return dispatch(actions.displayWarning(err.message))
} }
dispatch({ dispatch({
@ -409,7 +410,7 @@ function sendTx (txData) {
background.approveTransaction(txData.id, (err) => { background.approveTransaction(txData.id, (err) => {
if (err) { if (err) {
dispatch(actions.txError(err)) dispatch(actions.txError(err))
return console.error(err.message) return log.error(err.message)
} }
dispatch(actions.completedTx(txData.id)) dispatch(actions.completedTx(txData.id))
}) })
@ -424,7 +425,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
dispatch(actions.txError(err)) dispatch(actions.txError(err))
return console.error(err.message) return log.error(err.message)
} }
dispatch(actions.completedTx(txData.id)) dispatch(actions.completedTx(txData.id))
}) })
@ -558,6 +559,11 @@ function lockMetamask () {
return callBackgroundThenUpdate(background.setLocked) return callBackgroundThenUpdate(background.setLocked)
} }
function setCurrentAccountTab (newTabName) {
log.debug(`background.setCurrentAccountTab: ${newTabName}`)
return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName)
}
function showAccountDetail (address) { function showAccountDetail (address) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
@ -965,6 +971,17 @@ function shapeShiftRequest (query, options, cb) {
// We hide loading indication. // We hide loading indication.
// If it errored, we show a warning. // If it errored, we show a warning.
// If it didn't, we update the state. // 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) { function callBackgroundThenUpdate (method, ...args) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())

View File

@ -21,7 +21,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
const ConfigScreen = require('./config') const ConfigScreen = require('./config')
const Import = require('./accounts/import') const Import = require('./accounts/import')
const InfoScreen = require('./info') const InfoScreen = require('./info')
const LoadingIndicator = require('./components/loading') const Loading = require('./components/loading')
const SandwichExpando = require('sandwich-expando') const SandwichExpando = require('sandwich-expando')
const MenuDroppo = require('menu-droppo') const MenuDroppo = require('menu-droppo')
const DropMenuItem = require('./components/drop-menu-item') const DropMenuItem = require('./components/drop-menu-item')
@ -64,7 +64,11 @@ function mapStateToProps (state) {
App.prototype.render = function () { App.prototype.render = function () {
var props = this.props 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') log.debug('Main ui render function')
return ( return (
@ -77,13 +81,16 @@ App.prototype.render = function () {
}, },
}, [ }, [
h(LoadingIndicator, { isLoading, loadingMessage }),
// app bar // app bar
this.renderAppBar(), this.renderAppBar(),
this.renderNetworkDropdown(), this.renderNetworkDropdown(),
this.renderDropdown(), this.renderDropdown(),
h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
}),
// panel content // panel content
h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), {
style: { style: {
@ -124,7 +131,7 @@ App.prototype.renderAppBar = function () {
background: props.isUnlocked ? 'white' : 'none', background: props.isUnlocked ? 'white' : 'none',
height: '36px', height: '36px',
position: 'relative', position: 'relative',
zIndex: 10, zIndex: 12,
}, },
}, [ }, [
@ -221,7 +228,7 @@ App.prototype.renderNetworkDropdown = function () {
onClickOutside: (event) => { onClickOutside: (event) => {
this.setState({ isNetworkMenuOpen: !isOpen }) this.setState({ isNetworkMenuOpen: !isOpen })
}, },
zIndex: 1, zIndex: 11,
style: { style: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
@ -300,7 +307,7 @@ App.prototype.renderDropdown = function () {
return h(MenuDroppo, { return h(MenuDroppo, {
isOpen: isOpen, isOpen: isOpen,
zIndex: 1, zIndex: 11,
onClickOutside: (event) => { onClickOutside: (event) => {
this.setState({ isMainMenuOpen: !isOpen }) this.setState({ isMainMenuOpen: !isOpen })
}, },

View File

@ -20,8 +20,6 @@ function mapStateToProps (state) {
} }
ExportAccountView.prototype.render = function () { ExportAccountView.prototype.render = function () {
console.log('EXPORT VIEW')
console.dir(this.props)
var state = this.props var state = this.props
var accountDetail = state.accountDetail var accountDetail = state.accountDetail

View File

@ -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,
]))
)
}

View File

@ -23,7 +23,9 @@ IdenticonComponent.prototype.render = function () {
h('div', { h('div', {
key: 'identicon-' + this.props.address, key: 'identicon-' + this.props.address,
style: { style: {
display: 'inline-block', display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: diameter, height: diameter,
width: diameter, width: diameter,
borderRadius: diameter / 2, borderRadius: diameter / 2,
@ -35,21 +37,22 @@ IdenticonComponent.prototype.render = function () {
IdenticonComponent.prototype.componentDidMount = function () { IdenticonComponent.prototype.componentDidMount = function () {
var props = this.props var props = this.props
var address = props.address const { address } = props
if (!address) return if (!address) return
var container = findDOMNode(this) var container = findDOMNode(this)
var diameter = props.diameter || this.defaultDiameter var diameter = props.diameter || this.defaultDiameter
if (!isNode) { if (!isNode) {
var img = iconFactory.iconForAddress(address, diameter, false) var img = iconFactory.iconForAddress(address, diameter)
container.appendChild(img) container.appendChild(img)
} }
} }
IdenticonComponent.prototype.componentDidUpdate = function () { IdenticonComponent.prototype.componentDidUpdate = function () {
var props = this.props var props = this.props
var address = props.address const { address } = props
if (!address) return if (!address) return
@ -62,7 +65,8 @@ IdenticonComponent.prototype.componentDidUpdate = function () {
var diameter = props.diameter || this.defaultDiameter var diameter = props.diameter || this.defaultDiameter
if (!isNode) { if (!isNode) {
var img = iconFactory.iconForAddress(address, diameter, false) var img = iconFactory.iconForAddress(address, diameter)
container.appendChild(img) container.appendChild(img)
} }
} }

View File

@ -26,18 +26,21 @@ LoadingIndicator.prototype.render = function () {
style: { style: {
zIndex: 10, zIndex: 10,
position: 'absolute', position: 'absolute',
flexDirection: 'column',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
height: '100%', height: '100%',
width: '100%', width: '100%',
background: 'rgba(255, 255, 255, 0.5)', background: 'rgba(255, 255, 255, 0.8)',
}, },
}, [ }, [
h('img', { h('img', {
src: 'images/loading.svg', src: 'images/loading.svg',
}), }),
h('br'),
showMessageIfAny(loadingMessage), showMessageIfAny(loadingMessage),
]) : null, ]) : null,
]) ])

View File

@ -27,6 +27,7 @@ function PendingTx () {
this.state = { this.state = {
valid: true, valid: true,
txData: null, txData: null,
submitting: false,
} }
} }
@ -316,7 +317,7 @@ PendingTx.prototype.render = function () {
type: 'submit', type: 'submit',
value: 'ACCEPT', value: 'ACCEPT',
style: { marginLeft: '10px' }, style: { marginLeft: '10px' },
disabled: insufficientBalance || !this.state.valid || !isValidAddress, disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting,
}), }),
h('button.cancel.btn-red', { h('button.cancel.btn-red', {
@ -412,11 +413,12 @@ PendingTx.prototype.onSubmit = function (event) {
event.preventDefault() event.preventDefault()
const txMeta = this.gatherTxMeta() const txMeta = this.gatherTxMeta()
const valid = this.checkValidity() const valid = this.checkValidity()
this.setState({ valid }) this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) { if (valid && this.verifyGasParams()) {
this.props.sendTransaction(txMeta, event) this.props.sendTransaction(txMeta, event)
} else { } else {
this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
this.setState({ submitting: false })
} }
} }

View File

@ -33,3 +33,4 @@ TabBar.prototype.render = function () {
})) }))
) )
} }

View File

@ -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}`
}

View File

@ -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()
}

View File

@ -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', { h('.tx-list', {
style: { style: {
overflowY: 'auto', overflowY: 'auto',

View File

@ -101,14 +101,12 @@ InfoScreen.prototype.render = function () {
h('a.info', { h('a.info', {
href: 'https://github.com/MetaMask/faq', href: 'https://github.com/MetaMask/faq',
target: '_blank', target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, 'Need Help? Read our FAQ!'), }, 'Need Help? Read our FAQ!'),
]), ]),
h('div', [ h('div', [
h('a', { h('a', {
href: 'https://metamask.io/', href: 'https://metamask.io/',
target: '_blank', target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, [ }, [
h('img.icon-size', { h('img.icon-size', {
src: 'images/icon-128.png', src: 'images/icon-128.png',
@ -126,7 +124,6 @@ InfoScreen.prototype.render = function () {
h('a.info', { h('a.info', {
href: 'http://slack.metamask.io', href: 'http://slack.metamask.io',
target: '_blank', target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, 'Join the conversation on Slack'), }, 'Join the conversation on Slack'),
]), ]),
@ -134,7 +131,6 @@ InfoScreen.prototype.render = function () {
h('a.info', { h('a.info', {
href: 'https://twitter.com/metamask_io', href: 'https://twitter.com/metamask_io',
target: '_blank', target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, 'Follow us on Twitter'), }, 'Follow us on Twitter'),
]), ]),
@ -142,7 +138,7 @@ InfoScreen.prototype.render = function () {
h('a.info', { h('a.info', {
target: '_blank', target: '_blank',
style: { width: '85vw' }, style: { width: '85vw' },
onClick () { this.navigateTo('mailto:help@metamask.io?subject=Feedback') }, href: 'mailto:help@metamask.io?subject=Feedback',
}, 'Email us!'), }, 'Email us!'),
]), ]),
]), ]),
@ -155,3 +151,4 @@ InfoScreen.prototype.render = function () {
InfoScreen.prototype.navigateTo = function (url) { InfoScreen.prototype.navigateTo = function (url) {
global.platform.openWindow({ url }) global.platform.openWindow({ url })
} }

View File

@ -54,7 +54,7 @@ CreateVaultCompleteScreen.prototype.render = function () {
textAlign: 'center', textAlign: 'center',
}, },
}, [ }, [
h('span.error', 'These 12 words can restore all of your MetaMask accounts for this vault.\nSave them somewhere safe and secret.'), h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
]), ]),
h('textarea.twelve-word-phrase', { h('textarea.twelve-word-phrase', {

View File

@ -20,6 +20,7 @@ IconFactory.prototype.iconForAddress = function (address, diameter) {
if (iconExistsFor(addr)) { if (iconExistsFor(addr)) {
return imageElFor(addr) return imageElFor(addr)
} }
return this.generateIdenticonSvg(address, diameter) return this.generateIdenticonSvg(address, diameter)
} }
@ -43,7 +44,7 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) {
// util // util
function iconExistsFor (address) { function iconExistsFor (address) {
return (contractMap.address) && isValidAddress(address) && (contractMap[address].logo) return contractMap[address] && isValidAddress(address) && contractMap[address].logo
} }
function imageElFor (address) { function imageElFor (address) {
@ -52,7 +53,7 @@ function imageElFor (address) {
const path = `images/contract/${fileName}` const path = `images/contract/${fileName}`
const img = document.createElement('img') const img = document.createElement('img')
img.src = path img.src = path
img.style.width = '100%' img.style.width = '75%'
return img return img
} }