1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #3007 from alextsg/uat-master-011618

[NewUI] Merge master into uat branch
This commit is contained in:
Chi Kei Chan 2018-01-17 13:48:16 -08:00 committed by GitHub
commit b80ed2c451
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 395 additions and 84 deletions

View File

@ -2,10 +2,24 @@
## Current Master ## Current Master
## 3.13.5 2018-1-16
- Estimating gas limit for simple ether sends now faster & cheaper, by avoiding VM usage on recipients with no code.
- Add an extra px to address for Firefox clipping.
- Fix Firefox scrollbar.
- Open metamask popup for transaction confirmation before gas estimation finishes and add a loading screen over transaction confirmation.
- Fix bug that prevented eth_signTypedData from signing bytes.
- Further improve gas price estimation.
## 3.13.4 2018-1-9
- Remove recipient field if application initializes a tx with an empty string, or 0x, and tx data. Throw an error with the same condition, but without tx data.
- Improve gas price suggestion to be closer to the lowest that will be accepted.
- Throw an error if a application tries to submit a tx whose value is a decimal, and inform that it should be in wei. - Throw an error if a application tries to submit a tx whose value is a decimal, and inform that it should be in wei.
- Fix bug that prevented updating custom token details. - Fix bug that prevented updating custom token details.
- No longer mark long-pending transactions as failed, since we now have button to retry with higher gas. - No longer mark long-pending transactions as failed, since we now have button to retry with higher gas.
- Fix rounding error when specifying an ether amount that has too much precision. - Fix rounding error when specifying an ether amount that has too much precision.
- Fix bug where incorrectly inputting seed phrase would prevent any future attempts from succeeding.
## 3.13.3 2017-12-14 ## 3.13.3 2017-12-14

View File

@ -4,7 +4,7 @@
## Support ## Support
If you're a user seeking support, [here is our support site](http://metamask.consensyssupport.happyfox.com). If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/).
## Developing Compatible Dapps ## Developing Compatible Dapps

View File

@ -0,0 +1,10 @@
{
"appName": {
"message": "MetaMask",
"description": "The name of the application"
},
"appDescription": {
"message": "이더리움 계좌 관리",
"description": "The description of the application"
}
}

View File

@ -57,3 +57,4 @@ class BlacklistController {
} }
module.exports = BlacklistController module.exports = BlacklistController

View File

@ -1,6 +1,7 @@
const assert = require('assert') const assert = require('assert')
const EventEmitter = require('events') const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.js') const createMetamaskProvider = require('web3-provider-engine/zero.js')
const SubproviderFromProvider = require('web3-provider-engine/subproviders/web3.js')
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider') const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed') const ComposedStore = require('obs-store/lib/composed')
@ -161,15 +162,17 @@ module.exports = class NetworkController extends EventEmitter {
_configureInfuraProvider (opts) { _configureInfuraProvider (opts) {
log.info('_configureInfuraProvider', opts) log.info('_configureInfuraProvider', opts)
const blockTrackerProvider = createInfuraProvider({ const infuraProvider = createInfuraProvider({
network: opts.type, network: opts.type,
}) })
const infuraSubprovider = new SubproviderFromProvider(infuraProvider)
const providerParams = extend(this._baseProviderParams, { const providerParams = extend(this._baseProviderParams, {
rpcUrl: opts.rpcUrl, rpcUrl: opts.rpcUrl,
engineParams: { engineParams: {
pollingInterval: 8000, pollingInterval: 8000,
blockTrackerProvider, blockTrackerProvider: infuraProvider,
}, },
dataSubprovider: infuraSubprovider,
}) })
const provider = createMetamaskProvider(providerParams) const provider = createMetamaskProvider(providerParams)
this._setProvider(provider) this._setProvider(provider)

View File

@ -1,11 +1,14 @@
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const extend = require('xtend') const extend = require('xtend')
const BN = require('ethereumjs-util').BN
const EthQuery = require('eth-query')
class RecentBlocksController { class RecentBlocksController {
constructor (opts = {}) { constructor (opts = {}) {
const { blockTracker } = opts const { blockTracker, provider } = opts
this.blockTracker = blockTracker this.blockTracker = blockTracker
this.ethQuery = new EthQuery(provider)
this.historyLength = opts.historyLength || 40 this.historyLength = opts.historyLength || 40
const initState = extend({ const initState = extend({
@ -14,6 +17,7 @@ class RecentBlocksController {
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this.blockTracker.on('block', this.processBlock.bind(this)) this.blockTracker.on('block', this.processBlock.bind(this))
this.backfill()
} }
resetState () { resetState () {
@ -23,12 +27,7 @@ class RecentBlocksController {
} }
processBlock (newBlock) { processBlock (newBlock) {
const block = extend(newBlock, { const block = this.mapTransactionsToPrices(newBlock)
gasPrices: newBlock.transactions.map((tx) => {
return tx.gasPrice
}),
})
delete block.transactions
const state = this.store.getState() const state = this.store.getState()
state.recentBlocks.push(block) state.recentBlocks.push(block)
@ -39,6 +38,73 @@ class RecentBlocksController {
this.store.updateState(state) this.store.updateState(state)
} }
backfillBlock (newBlock) {
const block = this.mapTransactionsToPrices(newBlock)
const state = this.store.getState()
if (state.recentBlocks.length < this.historyLength) {
state.recentBlocks.unshift(block)
}
this.store.updateState(state)
}
mapTransactionsToPrices (newBlock) {
const block = extend(newBlock, {
gasPrices: newBlock.transactions.map((tx) => {
return tx.gasPrice
}),
})
delete block.transactions
return block
}
async backfill() {
this.blockTracker.once('block', async (block) => {
let blockNum = block.number
let recentBlocks
let state = this.store.getState()
recentBlocks = state.recentBlocks
while (recentBlocks.length < this.historyLength) {
try {
let blockNumBn = new BN(blockNum.substr(2), 16)
const newNum = blockNumBn.subn(1).toString(10)
const newBlock = await this.getBlockByNumber(newNum)
if (newBlock) {
this.backfillBlock(newBlock)
blockNum = newBlock.number
}
state = this.store.getState()
recentBlocks = state.recentBlocks
} catch (e) {
log.error(e)
}
await this.wait()
}
})
}
async wait () {
return new Promise((resolve) => {
setTimeout(resolve, 100)
})
}
async getBlockByNumber (number) {
const bn = new BN(number)
return new Promise((resolve, reject) => {
this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => {
if (err) reject(err)
resolve(block)
})
})
}
} }
module.exports = RecentBlocksController module.exports = RecentBlocksController

View File

@ -32,6 +32,7 @@ module.exports = class TransactionController extends EventEmitter {
this.provider = opts.provider this.provider = opts.provider
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction this.signEthTx = opts.signTransaction
this.getGasPrice = opts.getGasPrice
this.memStore = new ObservableStore({}) this.memStore = new ObservableStore({})
this.query = new EthQuery(this.provider) this.query = new EthQuery(this.provider)
@ -138,7 +139,6 @@ module.exports = class TransactionController extends EventEmitter {
async newUnapprovedTransaction (txParams) { async newUnapprovedTransaction (txParams) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams) const initialTxMeta = await this.addUnapprovedTransaction(txParams)
this.emit('newUnapprovedTx', initialTxMeta)
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => { this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
@ -166,11 +166,16 @@ module.exports = class TransactionController extends EventEmitter {
status: 'unapproved', status: 'unapproved',
metamaskNetworkId: this.getNetwork(), metamaskNetworkId: this.getNetwork(),
txParams: txParams, txParams: txParams,
loadingDefaults: true,
} }
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
// add default tx params // add default tx params
await this.addTxDefaults(txMeta) await this.addTxDefaults(txMeta)
txMeta.loadingDefaults = false
// save txMeta // save txMeta
this.addTx(txMeta) this.txStateManager.updateTx(txMeta)
return txMeta return txMeta
} }
@ -179,7 +184,10 @@ module.exports = class TransactionController extends EventEmitter {
// ensure value // ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
txMeta.nonceSpecified = Boolean(txParams.nonce) txMeta.nonceSpecified = Boolean(txParams.nonce)
const gasPrice = txParams.gasPrice || await this.query.gasPrice() let gasPrice = txParams.gasPrice
if (!gasPrice) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
}
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0' txParams.value = txParams.value || '0x0'
// set gasLimit // set gasLimit

View File

@ -4,6 +4,7 @@ const {
BnMultiplyByFraction, BnMultiplyByFraction,
bnToHex, bnToHex,
} = require('./util') } = require('./util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
/* /*
tx-utils are utility methods for Transaction manager tx-utils are utility methods for Transaction manager
@ -37,14 +38,30 @@ module.exports = class txProvideUtil {
async estimateTxGas (txMeta, blockGasLimitHex) { async estimateTxGas (txMeta, blockGasLimitHex) {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// check if gasLimit is already specified // check if gasLimit is already specified
txMeta.gasLimitSpecified = Boolean(txParams.gas) txMeta.gasLimitSpecified = Boolean(txParams.gas)
// if not, fallback to block gasLimit
if (!txMeta.gasLimitSpecified) { // if it is, use that value
if (txMeta.gasLimitSpecified) {
return txParams.gas
}
// if recipient has no code, gas is 21k max:
const recipient = txParams.to
const hasRecipient = Boolean(recipient)
const code = await this.query.getCode(recipient)
if (hasRecipient && (!code || code === '0x')) {
txParams.gas = SIMPLE_GAS_COST
txMeta.simpleSend = true // Prevents buffer addition
return SIMPLE_GAS_COST
}
// if not, fall back to block gasLimit
const blockGasLimitBN = hexToBn(blockGasLimitHex) const blockGasLimitBN = hexToBn(blockGasLimitHex)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20) const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
txParams.gas = bnToHex(saferGasLimitBN) txParams.gas = bnToHex(saferGasLimitBN)
}
// run tx // run tx
return await this.query.estimateGas(txParams) return await this.query.estimateGas(txParams)
} }
@ -55,7 +72,7 @@ module.exports = class txProvideUtil {
// if gasLimit was specified and doesnt OOG, // if gasLimit was specified and doesnt OOG,
// use original specified amount // use original specified amount
if (txMeta.gasLimitSpecified) { if (txMeta.gasLimitSpecified || txMeta.simpleSend) {
txMeta.estimatedGas = txParams.gas txMeta.estimatedGas = txParams.gas
return return
} }
@ -81,6 +98,7 @@ module.exports = class txProvideUtil {
} }
async validateTxParams (txParams) { async validateTxParams (txParams) {
this.validateRecipient(txParams)
if ('value' in txParams) { if ('value' in txParams) {
const value = txParams.value.toString() const value = txParams.value.toString()
if (value.includes('-')) { if (value.includes('-')) {
@ -92,4 +110,14 @@ module.exports = class txProvideUtil {
} }
} }
} }
validateRecipient (txParams) {
if (txParams.to === '0x') {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
}
return txParams
}
} }

View File

@ -5,7 +5,6 @@ const Dnode = require('dnode')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const asStream = require('obs-store/lib/asStream') const asStream = require('obs-store/lib/asStream')
const AccountTracker = require('./lib/account-tracker') const AccountTracker = require('./lib/account-tracker')
const EthQuery = require('eth-query')
const RpcEngine = require('json-rpc-engine') const RpcEngine = require('json-rpc-engine')
const debounce = require('debounce') const debounce = require('debounce')
const createEngineStream = require('json-rpc-middleware-stream/engineStream') const createEngineStream = require('json-rpc-middleware-stream/engineStream')
@ -35,13 +34,15 @@ const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url') const getBuyEthUrl = require('./lib/buy-eth-url')
const Mutex = require('await-semaphore').Mutex const Mutex = require('await-semaphore').Mutex
const version = require('../manifest.json').version const version = require('../manifest.json').version
const BN = require('ethereumjs-util').BN
const GWEI_BN = new BN('1000000000')
const percentile = require('percentile')
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200) this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
this.opts = opts this.opts = opts
@ -94,10 +95,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.recentBlocksController = new RecentBlocksController({ this.recentBlocksController = new RecentBlocksController({
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
provider: this.provider,
}) })
// eth data query tools
this.ethQuery = new EthQuery(this.provider)
// account tracker watches balances, nonces, and any code at their address. // account tracker watches balances, nonces, and any code at their address.
this.accountTracker = new AccountTracker({ this.accountTracker = new AccountTracker({
provider: this.provider, provider: this.provider,
@ -138,7 +138,7 @@ module.exports = class MetamaskController extends EventEmitter {
signTransaction: this.keyringController.signTransaction.bind(this.keyringController), signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider, provider: this.provider,
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
ethQuery: this.ethQuery, getGasPrice: this.getGasPrice.bind(this),
}) })
this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts)) this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
@ -489,6 +489,33 @@ module.exports = class MetamaskController extends EventEmitter {
this.emit('update', this.getState()) this.emit('update', this.getState())
} }
getGasPrice () {
const { recentBlocksController } = this
const { recentBlocks } = recentBlocksController.store.getState()
// Return 1 gwei if no blocks have been observed:
if (recentBlocks.length === 0) {
return '0x' + GWEI_BN.toString(16)
}
const lowestPrices = recentBlocks.map((block) => {
if (!block.gasPrices || block.gasPrices.length < 1) {
return GWEI_BN
}
return block.gasPrices
.map(hexPrefix => hexPrefix.substr(2))
.map(hex => new BN(hex, 16))
.sort((a, b) => {
return a.gt(b) ? 1 : -1
})[0]
})
.map(number => number.div(GWEI_BN).toNumber())
const percentileNum = percentile(50, lowestPrices)
const percentileNumBn = new BN(percentileNum)
return '0x' + percentileNumBn.mul(GWEI_BN).toString(16)
}
// //
// Vault Management // Vault Management
// //
@ -518,10 +545,15 @@ module.exports = class MetamaskController extends EventEmitter {
async createNewVaultAndRestore (password, seed) { async createNewVaultAndRestore (password, seed) {
const release = await this.createVaultMutex.acquire() const release = await this.createVaultMutex.acquire()
try {
const vault = await this.keyringController.createNewVaultAndRestore(password, seed) const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
this.selectFirstIdentity(vault) this.selectFirstIdentity(vault)
release() release()
return vault return vault
} catch (err) {
release()
throw err
}
} }
selectFirstIdentity (vault) { selectFirstIdentity (vault) {

View File

@ -19,6 +19,8 @@ var manifest = require('./app/manifest.json')
var gulpif = require('gulp-if') var gulpif = require('gulp-if')
var replace = require('gulp-replace') var replace = require('gulp-replace')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var asyncEach = require('async/each')
var exec = require('child_process').exec
var sass = require('gulp-sass') var sass = require('gulp-sass')
var autoprefixer = require('gulp-autoprefixer') var autoprefixer = require('gulp-autoprefixer')
var gulpStylelint = require('gulp-stylelint') var gulpStylelint = require('gulp-stylelint')
@ -161,6 +163,18 @@ gulp.task('copy:watch', function(){
gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy')) gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy'))
}) })
// record deps
gulp.task('deps', function (cb) {
exec('npm ls', (err, stdoutOutput, stderrOutput) => {
if (err) return cb(err)
const browsers = ['firefox','chrome','edge','opera']
asyncEach(browsers, (target, done) => {
fs.writeFile(`./dist/${target}/deps.txt`, stdoutOutput, done)
}, cb)
})
})
// lint js // lint js
gulp.task('lint', function () { gulp.task('lint', function () {
@ -289,7 +303,7 @@ gulp.task('apply-prod-environment', function(done) {
gulp.task('dev', gulp.series('build:scss', 'dev:js', 'copy', gulp.parallel('watch:scss', 'copy:watch', 'dev:reload'))) gulp.task('dev', gulp.series('build:scss', 'dev:js', 'copy', gulp.parallel('watch:scss', 'copy:watch', 'dev:reload')))
gulp.task('build', gulp.series('clean', 'build:scss', gulp.parallel('build:js', 'copy'))) gulp.task('build', gulp.series('clean', 'build:scss', gulp.parallel('build:js', 'copy', 'deps')))
gulp.task('dist', gulp.series('apply-prod-environment', 'build', 'zip')) gulp.task('dist', gulp.series('apply-prod-environment', 'build', 'zip'))
// task generators // task generators

View File

@ -4,5 +4,3 @@ When you log in to MetaMask, your current account is visible to every new site y
For your privacy, for now, please sign out of MetaMask when you're done using a site. For your privacy, for now, please sign out of MetaMask when you're done using a site.
Also, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.

File diff suppressed because one or more lines are too long

View File

@ -78,9 +78,10 @@ AccountDetailScreen.prototype.render = function () {
address: selected, address: selected,
}), }),
]), ]),
h('div.flex-column', { h('flex-column', {
style: { style: {
lineHeight: '10px', lineHeight: '10px',
marginLeft: '15px',
width: '100%', width: '100%',
}, },
}, [ }, [
@ -101,7 +102,7 @@ AccountDetailScreen.prototype.render = function () {
{ {
style: { style: {
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'flex-start',
alignItems: 'center', alignItems: 'center',
}, },
}, },
@ -131,6 +132,8 @@ AccountDetailScreen.prototype.render = function () {
AccountDropdowns, AccountDropdowns,
{ {
style: { style: {
marginRight: '8px',
marginLeft: 'auto',
cursor: 'pointer', cursor: 'pointer',
}, },
selected, selected,
@ -144,6 +147,7 @@ AccountDetailScreen.prototype.render = function () {
]), ]),
h('.flex-row', { h('.flex-row', {
style: { style: {
width: '15em',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'baseline', alignItems: 'baseline',
}, },
@ -157,12 +161,11 @@ AccountDetailScreen.prototype.render = function () {
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
paddingTop: '3px', paddingTop: '3px',
width: '5em', width: '5em',
height: '15px',
fontSize: '13px', fontSize: '13px',
fontFamily: 'Montserrat Light', fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision', textRendering: 'geometricPrecision',
marginTop: '15px',
marginBottom: '15px', marginBottom: '15px',
marginLeft: '15px',
color: '#AEAEAE', color: '#AEAEAE',
}, },
}, checksumAddress), }, checksumAddress),
@ -189,7 +192,7 @@ AccountDetailScreen.prototype.render = function () {
}, },
}), }),
h('div', {}, [ h('.flex-grow'),
h('button', { h('button', {
onClick: () => props.dispatch(actions.buyEthView(selected)), onClick: () => props.dispatch(actions.buyEthView(selected)),
@ -200,12 +203,11 @@ AccountDetailScreen.prototype.render = function () {
onClick: () => props.dispatch(actions.showSendPage()), onClick: () => props.dispatch(actions.showSendPage()),
style: { style: {
marginBottom: '20px', marginBottom: '20px',
marginRight: '8px',
}, },
}, 'SEND'), }, 'SEND'),
]), ]),
]),
]), ]),
// subview (tx history, pk export confirm, buy eth warning) // subview (tx history, pk export confirm, buy eth warning)

View File

@ -397,7 +397,7 @@ App.prototype.renderDropdown = function () {
h(DropdownMenuItem, { h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.lockMetamask()) }, onClick: () => { this.props.dispatch(actions.lockMetamask()) },
}, 'Lock'), }, 'Log Out'),
h(DropdownMenuItem, { h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
@ -470,11 +470,6 @@ App.prototype.renderPrimary = function () {
}) })
} }
if (props.seedWords) {
log.debug('rendering seed words')
return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
}
// show initialize screen // show initialize screen
if (!props.isInitialized || props.forgottenPassword) { if (!props.isInitialized || props.forgottenPassword) {
// show current view // show current view
@ -509,6 +504,12 @@ App.prototype.renderPrimary = function () {
} }
} }
// show seed words screen
if (props.seedWords) {
log.debug('rendering seed words')
return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
}
// show current view // show current view
switch (props.currentView.name) { switch (props.currentView.name) {

View File

@ -4,6 +4,7 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../ui/app/actions') const actions = require('../../ui/app/actions')
const NetworkIndicator = require('./components/network') const NetworkIndicator = require('./components/network')
const LoadingIndicator = require('./components/loading')
const txHelper = require('../lib/tx-helper') const txHelper = require('../lib/tx-helper')
const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification')
@ -60,6 +61,11 @@ ConfirmTxScreen.prototype.render = function () {
h('.flex-column.flex-grow', [ h('.flex-column.flex-grow', [
h(LoadingIndicator, {
isLoading: txData.loadingDefaults,
loadingMessage: 'Estimating transaction cost…',
}),
// subtitle and nav // subtitle and nav
h('.section-title.flex-row.flex-center', [ h('.section-title.flex-row.flex-center', [
!isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {

View File

@ -440,7 +440,9 @@ input.large-input {
.account-detail-section { .account-detail-section {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
max-height: 465px;
flex-direction: inherit; flex-direction: inherit;
} }

View File

@ -77,15 +77,14 @@
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.2.0", "eth-block-tracker": "^2.2.0",
"eth-json-rpc-filters": "^1.2.5",
"eth-json-rpc-infura": "^2.0.5",
"eth-keyring-controller": "^2.1.4",
"eth-contract-metadata": "^1.1.5", "eth-contract-metadata": "^1.1.5",
"eth-hd-keyring": "^1.2.1", "eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.4",
"eth-json-rpc-infura": "^1.0.2",
"eth-keyring-controller": "^2.1.3",
"eth-phishing-detect": "^1.1.4", "eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.4.0", "eth-sig-util": "^1.4.2",
"eth-simple-keyring": "^1.2.0",
"eth-token-tracker": "^1.1.4", "eth-token-tracker": "^1.1.4",
"ethereumjs-abi": "^0.6.4", "ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "^1.3.0", "ethereumjs-tx": "^1.3.0",
@ -130,6 +129,7 @@
"obj-multiplex": "^1.0.0", "obj-multiplex": "^1.0.0",
"obs-store": "^3.0.0", "obs-store": "^3.0.0",
"once": "^1.3.3", "once": "^1.3.3",
"percentile": "^1.2.0",
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
"pojo-migrator": "^2.1.0", "pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0", "polyfill-crypto.getrandomvalues": "^1.0.0",
@ -169,7 +169,7 @@
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "^0.20.1", "web3": "^0.20.1",
"web3-provider-engine": "^13.4.0", "web3-provider-engine": "^13.5.0",
"web3-stream-provider": "^3.0.1", "web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
@ -212,6 +212,7 @@
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5", "gulp-watch": "^4.3.5",
"gulp-zip": "^4.0.0", "gulp-zip": "^4.0.0",
"gulp-eslint": "^4.0.0",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsdom": "^11.1.0", "jsdom": "^11.1.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
@ -241,7 +242,7 @@
"tape": "^4.5.1", "tape": "^4.5.1",
"testem": "^1.10.3", "testem": "^1.10.3",
"uglifyify": "^4.0.2", "uglifyify": "^4.0.2",
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"watchify": "^3.9.0" "watchify": "^3.9.0"
}, },

View File

@ -5,7 +5,8 @@ module.exports = {
createEngineForTestData, createEngineForTestData,
providerFromEngine, providerFromEngine,
scaffoldMiddleware, scaffoldMiddleware,
createStubedProvider createEthJsQueryStub,
createStubedProvider,
} }
@ -18,6 +19,18 @@ function providerFromEngine (engine) {
return provider return provider
} }
function createEthJsQueryStub (stubProvider) {
return new Proxy({}, {
get: (obj, method) => {
return (...params) => {
return new Promise((resolve, reject) => {
stubProvider.sendAsync({ method: `eth_${method}`, params }, (err, ress) => resolve(ress.result))
})
}
},
})
}
function createStubedProvider (resultStub) { function createStubedProvider (resultStub) {
const engine = createEngineForTestData() const engine = createEngineForTestData()
engine.push(scaffoldMiddleware(resultStub)) engine.push(scaffoldMiddleware(resultStub))

View File

@ -3,6 +3,8 @@ const sinon = require('sinon')
const clone = require('clone') const clone = require('clone')
const MetaMaskController = require('../../app/scripts/metamask-controller') const MetaMaskController = require('../../app/scripts/metamask-controller')
const firstTimeState = require('../../app/scripts/first-time-state') const firstTimeState = require('../../app/scripts/first-time-state')
const BN = require('ethereumjs-util').BN
const GWEI_BN = new BN('1000000000')
describe('MetaMaskController', function () { describe('MetaMaskController', function () {
const noop = () => {} const noop = () => {}
@ -39,17 +41,63 @@ describe('MetaMaskController', function () {
beforeEach(function () { beforeEach(function () {
sinon.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') sinon.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sinon.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
}) })
afterEach(function () { afterEach(function () {
metamaskController.keyringController.createNewVaultAndKeychain.restore() metamaskController.keyringController.createNewVaultAndKeychain.restore()
metamaskController.keyringController.createNewVaultAndRestore.restore()
})
describe('#getGasPrice', function () {
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: [
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
]
}
}
}
}
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
metamaskController.recentBlocksController = realRecentBlocksController
})
it('gives the 1 gwei price if no blocks have been seen.', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: []
}
}
}
}
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x' + GWEI_BN.toString(16), 'defaults to 1 gwei')
metamaskController.recentBlocksController = realRecentBlocksController
})
}) })
describe('#createNewVaultAndKeychain', function () { describe('#createNewVaultAndKeychain', function () {
it('can only create new vault on keyringController once', async function () { it('can only create new vault on keyringController once', async function () {
const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity') const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity')
const password = 'a-fake-password' const password = 'a-fake-password'
const first = await metamaskController.createNewVaultAndKeychain(password) const first = await metamaskController.createNewVaultAndKeychain(password)
@ -60,6 +108,22 @@ describe('MetaMaskController', function () {
selectStub.reset() selectStub.reset()
}) })
}) })
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
// const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity')
const password = 'what-what-what'
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu'
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
const first = await metamaskController.createNewVaultAndRestore(password, wrongSeed)
.catch((e) => {
return
})
const second = await metamaskController.createNewVaultAndRestore(password, rightSeed)
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
})
})
}) })
}) })

View File

@ -5,7 +5,7 @@ const ObservableStore = require('obs-store')
const sinon = require('sinon') const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions') const TransactionController = require('../../app/scripts/controllers/transactions')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createStubedProvider } = require('../stub/provider') const { createStubedProvider, createEthJsQueryStub } = require('../stub/provider')
const noop = () => true const noop = () => true
const currentNetworkId = 42 const currentNetworkId = 42
@ -30,6 +30,8 @@ describe('Transaction Controller', function () {
resolve() resolve()
}), }),
}) })
txController.query = createEthJsQueryStub(provider)
txController.txGasUtil.query = createEthJsQueryStub(provider)
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
txController.txProviderUtils = new TxGasUtils(txController.provider) txController.txProviderUtils = new TxGasUtils(txController.provider)
}) })
@ -110,22 +112,15 @@ describe('Transaction Controller', function () {
history: [], history: [],
} }
txController.txStateManager._saveTxList([txMeta]) txController.txStateManager._saveTxList([txMeta])
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txController.txStateManager.addTx(txMeta))) stub = sinon.stub(txController, 'addUnapprovedTransaction').callsFake(() => {
txController.emit('newUnapprovedTx', txMeta)
return Promise.resolve(txController.txStateManager.addTx(txMeta))
}) })
afterEach(function () { afterEach(function () {
txController.txStateManager._saveTxList([]) txController.txStateManager._saveTxList([])
stub.restore() stub.restore()
}) })
it('should emit newUnapprovedTx event and pass txMeta as the first argument', function (done) {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
assert(txMetaFromEmit, 'txMeta is falsey')
assert.equal(txMetaFromEmit.id, 1, 'the right txMeta was passed')
done()
})
txController.newUnapprovedTransaction(txParams)
.catch(done)
}) })
it('should resolve when finished and status is submitted and resolve with the hash', function (done) { it('should resolve when finished and status is submitted and resolve with the hash', function (done) {
@ -160,8 +155,17 @@ describe('Transaction Controller', function () {
}) })
describe('#addUnapprovedTransaction', function () { describe('#addUnapprovedTransaction', function () {
let addTxDefaults
beforeEach(() => {
addTxDefaults = txController.addTxDefaults
txController.addTxDefaults = function addTxDefaultsStub () { return Promise.resolve() }
})
afterEach(() => {
txController.addTxDefaults = addTxDefaults
})
it('should add an unapproved transaction and return a valid txMeta', function (done) { it('should add an unapproved transaction and return a valid txMeta', function (done) {
const addTxDefaultsStub = sinon.stub(txController, 'addTxDefaults').callsFake(() => Promise.resolve())
txController.addUnapprovedTransaction({}) txController.addUnapprovedTransaction({})
.then((txMeta) => { .then((txMeta) => {
assert(('id' in txMeta), 'should have a id') assert(('id' in txMeta), 'should have a id')
@ -172,10 +176,20 @@ describe('Transaction Controller', function () {
const memTxMeta = txController.txStateManager.getTx(txMeta.id) const memTxMeta = txController.txStateManager.getTx(txMeta.id)
assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`) assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`)
addTxDefaultsStub.restore()
done() done()
}).catch(done) }).catch(done)
}) })
it('should emit newUnapprovedTx event and pass txMeta as the first argument', function (done) {
providerResultStub.eth_gasPrice = '4a817c800'
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
assert(txMetaFromEmit, 'txMeta is falsey')
done()
})
txController.addUnapprovedTransaction({})
.catch(done)
})
}) })
describe('#addTxDefaults', function () { describe('#addTxDefaults', function () {

View File

@ -0,0 +1,32 @@
const assert = require('assert')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createStubedProvider } = require('../stub/provider')
describe('Tx Gas Util', function () {
let txGasUtil, provider, providerResultStub
beforeEach(function () {
providerResultStub = {}
provider = createStubedProvider(providerResultStub)
txGasUtil = new TxGasUtils({
provider,
})
})
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txGasUtil.validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
})
it('should error when recipient is 0x', function () {
const zeroRecipientTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
}
assert.throws(() => { txGasUtil.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
})
})

View File

@ -302,7 +302,6 @@ App.prototype.renderAppBar = function () {
) )
} }
App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) { App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props const { isMascara } = this.props

View File

@ -103,9 +103,9 @@ InfoScreen.prototype.render = function () {
[ [
h('div.fa.fa-support', [ h('div.fa.fa-support', [
h('a.info', { h('a.info', {
href: 'https://support.metamask.io', href: 'https://metamask.helpscoutdocs.com/',
target: '_blank', target: '_blank',
}, 'Visit our Support Center'), }, 'Visit our Knowledge Base'),
]), ]),
h('div', [ h('div', [
@ -138,8 +138,7 @@ InfoScreen.prototype.render = function () {
h('div.fa.fa-envelope', [ h('div.fa.fa-envelope', [
h('a.info', { h('a.info', {
target: '_blank', target: '_blank',
style: { width: '85vw' }, href: 'mailto:support@metamask.io?subject=MetaMask Support',
href: 'mailto:help@metamask.io?subject=Feedback',
}, 'Email us!'), }, 'Email us!'),
]), ]),
]), ]),

View File

@ -149,4 +149,8 @@ RestoreVaultScreen.prototype.createNewVaultAndRestore = function () {
this.warning = null this.warning = null
this.props.dispatch(actions.displayWarning(this.warning)) this.props.dispatch(actions.displayWarning(this.warning))
this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) this.props.dispatch(actions.createNewVaultAndRestore(password, seed))
.catch((err) => {
log.error(err.message)
})
} }