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"],
"plugins": ["transform-runtime"]
"presets": ["es2015", "stage-0"],
"plugins": ["transform-runtime", "transform-async-to-generator"]
}

View File

@ -2,8 +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

View File

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

View File

@ -384,13 +384,13 @@ 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
this.emit(`${txMeta.id}:${status}`, txId)
if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta)
this.emit('updateBadge')

View File

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

View File

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

View File

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

View File

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

View File

@ -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\"",
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
@ -62,11 +62,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",
@ -128,8 +129,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",

View File

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

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 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,10 +241,43 @@ 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,

View File

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

View File

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

View File

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

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', {
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)
}
}

View File

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

View File

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

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', {
style: {
overflowY: 'auto',

View File

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

View File

@ -54,7 +54,7 @@ CreateVaultCompleteScreen.prototype.render = function () {
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', {

View File

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