mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'master' into i1473-dappDefaultGasPrice
This commit is contained in:
commit
1977417017
4
.babelrc
4
.babelrc
@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["transform-runtime"]
|
||||
"presets": ["es2015", "stage-0"],
|
||||
"plugins": ["transform-runtime", "transform-async-to-generator"]
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
42
app/scripts/controllers/infura.js
Normal file
42
app/scripts/controllers/infura.js
Normal 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
|
@ -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 })
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
@ -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),
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
machine:
|
||||
node:
|
||||
version: 7.6.0
|
||||
version: 8.0.0
|
||||
dependencies:
|
||||
pre:
|
||||
- "npm i -g testem"
|
||||
|
16
gulpfile.js
16
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()))
|
||||
|
||||
)
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
34
test/unit/infura-controller-test.js
Normal file
34
test/unit/infura-controller-test.js
Normal 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)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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 })
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
|
89
ui/app/components/balance.js
Normal file
89
ui/app/components/balance.js
Normal 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,
|
||||
]))
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
])
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,3 +33,4 @@ TabBar.prototype.render = function () {
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
|
46
ui/app/components/token-cell.js
Normal file
46
ui/app/components/token-cell.js
Normal 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}`
|
||||
}
|
||||
|
147
ui/app/components/token-list.js
Normal file
147
ui/app/components/token-list.js
Normal 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()
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
|
@ -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', {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user