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

Merge branch 'master' into nm

This commit is contained in:
Chi Kei Chan 2017-09-18 11:28:10 -07:00
commit 6c5865d564
61 changed files with 4489 additions and 611 deletions

View File

@ -2,6 +2,61 @@
## Current Master ## Current Master
- Add ability to export private keys as a file.
- Add ability to export seed words as a file.
- Changed state logs to a file download than a clipboard copy.
- Fixed a long standing memory leak associated with filters installed by dapps
- Fix link to support center.
## 3.10.0 2017-9-11
- Readded loose keyring label back into the account list.
- Remove cryptonator from chrome permissions.
- Add info on token contract addresses.
- Add validation preventing users from inputting their own addresses as token tracking addresses.
- Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94)
## 3.9.13 2017-9-8
- Changed the way we initialize the inpage provider to fix a bug affecting some developers.
## 3.9.12 2017-9-6
- Fix bug that prevented Web3 1.0 compatibility
- Make eth_sign deprecation warning less noisy
- Add useful link to eth_sign deprecation warning.
- Fix bug with network version serialization over synchronous RPC
- Add MetaMask version to state logs.
- Add the total amount of tokens when multiple tokens are added under the token list
- Use HTTPS links for Etherscan.
- Update Support center link to new one with HTTPS.
- Make web3 deprecation notice more useful by linking to a descriptive article.
## 3.9.11 2017-8-24
- Fix nonce calculation bug that would sometimes generate very wrong nonces.
- Give up resubmitting a transaction after 3500 blocks.
## 3.9.10 2017-8-23
- Improve nonce calculation, to prevent bug where people are unable to send transactions reliably.
- Remove link to eth-tx-viz from identicons in tx history.
## 3.9.9 2017-8-18
- Fix bug where some transaction submission errors would show an empty screen.
- Fix bug that could mis-render token balances when very small.
- Fix formatting of eth_sign "Sign Message" view.
- Add deprecation warning to eth_sign "Sign Message" view.
## 3.9.8 2017-8-16
- Reenable token list.
- Remove default tokens.
## 3.9.7 2017-8-15
- hotfix - disable token list
- Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0 - Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0
## 3.9.6 2017-8-09 ## 3.9.6 2017-8-09

View File

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.9.6", "version": "3.10.0",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
@ -57,8 +57,7 @@
"permissions": [ "permissions": [
"storage", "storage",
"clipboardWrite", "clipboardWrite",
"http://localhost:8545/", "http://localhost:8545/"
"https://api.cryptonator.com/"
], ],
"web_accessible_resources": [ "web_accessible_resources": [
"scripts/inpage.js" "scripts/inpage.js"

View File

@ -1,6 +1,8 @@
const urlUtil = require('url') const urlUtil = require('url')
const endOfStream = require('end-of-stream') const endOfStream = require('end-of-stream')
const pipe = require('pump') const pipe = require('pump')
const log = require('loglevel')
const extension = require('extensionizer')
const LocalStorageStore = require('obs-store/lib/localStorage') const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform') const storeTransform = require('obs-store/lib/transform')
const ExtensionPlatform = require('./platforms/extension') const ExtensionPlatform = require('./platforms/extension')
@ -9,13 +11,11 @@ const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const NotificationManager = require('./lib/notification-manager.js') const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller') const MetamaskController = require('./metamask-controller')
const extension = require('extensionizer')
const firstTimeState = require('./first-time-state') const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const log = require('loglevel')
window.log = log window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
@ -29,12 +29,12 @@ let popupIsOpen = false
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
// initialization flow // initialization flow
initialize().catch(console.error) initialize().catch(log.error)
async function initialize () { async function initialize () {
const initState = await loadStateFromPersistence() const initState = await loadStateFromPersistence()
await setupController(initState) await setupController(initState)
console.log('MetaMask initialization complete.') log.debug('MetaMask initialization complete.')
} }
// //

View File

@ -1,11 +1,12 @@
const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
const PortStream = require('./lib/port-stream.js')
const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('extensionizer')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const pump = require('pump')
const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
const PortStream = require('./lib/port-stream.js')
const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString() const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
// Eventually this streaming injection could be replaced with: // Eventually this streaming injection could be replaced with:
@ -50,22 +51,42 @@ function setupStreams () {
pageStream.pipe(pluginStream).pipe(pageStream) pageStream.pipe(pluginStream).pipe(pageStream)
// setup local multistream channels // setup local multistream channels
const mx = ObjectMultiplex() const mux = new ObjectMultiplex()
mx.on('error', console.error) pump(
mx.pipe(pageStream).pipe(mx) mux,
mx.pipe(pluginStream).pipe(mx) pageStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask Inpage', err)
)
pump(
mux,
pluginStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask Background', err)
)
// connect ping stream // connect ping stream
const pongStream = new PongStream({ objectMode: true }) const pongStream = new PongStream({ objectMode: true })
pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream) pump(
mux,
pongStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask PingPongStream', err)
)
// connect phishing warning stream // connect phishing warning stream
const phishingStream = mx.createStream('phishing') const phishingStream = mux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning) phishingStream.once('data', redirectToPhishingWarning)
// ignore unused channels (handled by background, inpage) // ignore unused channels (handled by background, inpage)
mx.ignoreStream('provider') mux.ignoreStream('provider')
mx.ignoreStream('publicConfig') mux.ignoreStream('publicConfig')
}
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
} }
function shouldInjectWeb3 () { function shouldInjectWeb3 () {

View File

@ -1,6 +1,5 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const extend = require('xtend') const extend = require('xtend')
const clone = require('clone')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
@ -8,6 +7,7 @@ const TxProviderUtil = require('../lib/tx-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker') const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id') const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('../lib/nonce-tracker')
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
module.exports = class TransactionController extends EventEmitter { module.exports = class TransactionController extends EventEmitter {
constructor (opts) { constructor (opts) {
@ -33,6 +33,17 @@ module.exports = class TransactionController extends EventEmitter {
err: undefined, err: undefined,
}) })
}, },
getConfirmedTransactions: (address) => {
return this.getFilteredTxList({
from: address,
status: 'confirmed',
err: undefined,
})
},
giveUpOnTransaction: (txId) => {
const msg = `Gave up submitting after 3500 blocks un-mined.`
this.setTxStatusFailed(txId, msg)
},
}) })
this.query = new EthQuery(this.provider) this.query = new EthQuery(this.provider)
this.txProviderUtil = new TxProviderUtil(this.provider) this.txProviderUtil = new TxProviderUtil(this.provider)
@ -128,19 +139,17 @@ module.exports = class TransactionController extends EventEmitter {
updateTx (txMeta) { updateTx (txMeta) {
// create txMeta snapshot for history // create txMeta snapshot for history
const txMetaForHistory = clone(txMeta) const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
// dont include previous history in this snapshot // recover previous tx state obj
delete txMetaForHistory.history const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
// add snapshot to tx history // generate history entry and add to history
if (!txMeta.history) txMeta.history = [] const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState)
txMeta.history.push(txMetaForHistory) txMeta.history.push(entry)
// commit txMeta to state
const txId = txMeta.id const txId = txMeta.id
const txList = this.getFullTxList() const txList = this.getFullTxList()
const index = txList.findIndex(txData => txData.id === txId) const index = txList.findIndex(txData => txData.id === txId)
if (!txMeta.history) txMeta.history = []
txMeta.history.push(txMetaForHistory)
txList[index] = txMeta txList[index] = txMeta
this._saveTxList(txList) this._saveTxList(txList)
this.emit('update') this.emit('update')
@ -148,16 +157,22 @@ module.exports = class TransactionController extends EventEmitter {
// Adds a tx to the txlist // Adds a tx to the txlist
addTx (txMeta) { addTx (txMeta) {
const txCount = this.getTxCount() // initialize history
const network = this.getNetwork() txMeta.history = []
const fullTxList = this.getFullTxList() // capture initial snapshot of txMeta for history
const txHistoryLimit = this.txHistoryLimit const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history.push(snapshot)
// checks if the length of the tx history is // checks if the length of the tx history is
// longer then desired persistence limit // longer then desired persistence limit
// and then if it is removes only confirmed // and then if it is removes only confirmed
// or rejected tx's. // or rejected tx's.
// not tx's that are pending or unapproved // not tx's that are pending or unapproved
const txCount = this.getTxCount()
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
const txHistoryLimit = this.txHistoryLimit
if (txCount > txHistoryLimit - 1) { if (txCount > txHistoryLimit - 1) {
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
fullTxList.splice(index, 1) fullTxList.splice(index, 1)
@ -206,7 +221,6 @@ module.exports = class TransactionController extends EventEmitter {
status: 'unapproved', status: 'unapproved',
metamaskNetworkId: this.getNetwork(), metamaskNetworkId: this.getNetwork(),
txParams: txParams, txParams: txParams,
history: [],
} }
// add default tx params // add default tx params
await this.addTxDefaults(txMeta) await this.addTxDefaults(txMeta)

View File

@ -171,9 +171,9 @@ class KeyringController extends EventEmitter {
return this.setupAccounts(checkedAccounts) return this.setupAccounts(checkedAccounts)
}) })
.then(() => this.persistAllKeyrings()) .then(() => this.persistAllKeyrings())
.then(() => this._updateMemStoreKeyrings())
.then(() => this.fullUpdate()) .then(() => this.fullUpdate())
.then(() => { .then(() => {
this._updateMemStoreKeyrings()
return keyring return keyring
}) })
} }
@ -208,6 +208,7 @@ class KeyringController extends EventEmitter {
return selectedKeyring.addAccounts(1) return selectedKeyring.addAccounts(1)
.then(this.setupAccounts.bind(this)) .then(this.setupAccounts.bind(this))
.then(this.persistAllKeyrings.bind(this)) .then(this.persistAllKeyrings.bind(this))
.then(this._updateMemStoreKeyrings.bind(this))
.then(this.fullUpdate.bind(this)) .then(this.fullUpdate.bind(this))
} }

View File

@ -2,33 +2,55 @@ module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) { function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage // export web3 as a global, checking for usage
let hasBeenWarned = false
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
global.web3 = new Proxy(web3, { global.web3 = new Proxy(web3, {
get: (_web3, name) => { get: (_web3, key) => {
// get the time of use // show warning once on web3 access
if (name !== '_used') { if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/ethereum/mist/releases/tag/v0.9.0') console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
_web3._used = Date.now() hasBeenWarned = true
} }
return _web3[name] // get the time of use
lastTimeUsed = Date.now()
// return value normally
return _web3[key]
}, },
set: (_web3, name, value) => { set: (_web3, key, value) => {
_web3[name] = value // set value normally
_web3[key] = value
}, },
}) })
var networkVersion
observable.subscribe(function (state) { observable.subscribe(function (state) {
// get the initial network // if reload in progress, no need to check reload logic
const curentNetVersion = state.networkVersion if (reloadInProgress) return
if (!networkVersion) networkVersion = curentNetVersion
if (curentNetVersion !== networkVersion && web3._used) { const currentNetwork = state.networkVersion
const timeSinceUse = Date.now() - web3._used
// set the initial network
if (!lastSeenNetwork) {
lastSeenNetwork = currentNetwork
return
}
// skip reload logic if web3 not used
if (!lastTimeUsed) return
// if network did not change, exit
if (currentNetwork === lastSeenNetwork) return
// initiate page reload
reloadInProgress = true
const timeSinceUse = Date.now() - lastTimeUsed
// if web3 was recently used then delay the reloading of the page // if web3 was recently used then delay the reloading of the page
timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500) if (timeSinceUse > 500) {
// prevent reentry into if statement if state updates again before triggerReset()
// reload } else {
networkVersion = curentNetVersion setTimeout(triggerReset, 500)
} }
}) })
} }

View File

@ -0,0 +1,15 @@
// log rpc activity
module.exports = createLoggerMiddleware
function createLoggerMiddleware({ origin }) {
return function loggerMiddleware (req, res, next, end) {
next((cb) => {
if (res.error) {
log.error('Error in RPC response:\n', res)
}
if (req.isMetamaskInternal) return
log.info(`RPC (${origin}):`, req, '->', res)
cb()
})
}
}

View File

@ -0,0 +1,9 @@
// append dapp origin domain to request
module.exports = createOriginMiddleware
function createOriginMiddleware({ origin }) {
return function originMiddleware (req, res, next, end) {
req.origin = origin
next()
}
}

View File

@ -0,0 +1,13 @@
module.exports = createProviderMiddleware
// forward requests to provider
function createProviderMiddleware({ provider }) {
return (req, res, next, end) => {
provider.sendAsync(req, (err, _res) => {
if (err) return end(err)
res.result = _res.result
end()
})
}
}

View File

@ -1,8 +1,9 @@
const pipe = require('pump') const pump = require('pump')
const StreamProvider = require('web3-stream-provider') const RpcEngine = require('json-rpc-engine')
const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
const createStreamMiddleware = require('json-rpc-middleware-stream')
const LocalStorageStore = require('obs-store') const LocalStorageStore = require('obs-store')
const ObjectMultiplex = require('./obj-multiplex') const ObjectMultiplex = require('obj-multiplex')
const createRandomId = require('./random-id')
module.exports = MetamaskInpageProvider module.exports = MetamaskInpageProvider
@ -10,60 +11,49 @@ function MetamaskInpageProvider (connectionStream) {
const self = this const self = this
// setup connectionStream multiplexing // setup connectionStream multiplexing
var multiStream = self.multiStream = ObjectMultiplex() const mux = self.mux = new ObjectMultiplex()
pipe( pump(
connectionStream, connectionStream,
multiStream, mux,
connectionStream, connectionStream,
(err) => logStreamDisconnectWarning('MetaMask', err) (err) => logStreamDisconnectWarning('MetaMask', err)
) )
// subscribe to metamask public config (one-way) // subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
pipe( pump(
multiStream.createStream('publicConfig'), mux.createStream('publicConfig'),
self.publicConfigStore, self.publicConfigStore,
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
) )
// ignore phishing warning message (handled elsewhere) // ignore phishing warning message (handled elsewhere)
multiStream.ignoreStream('phishing') mux.ignoreStream('phishing')
// connect to async provider // connect to async provider
const asyncProvider = self.asyncProvider = new StreamProvider() const streamMiddleware = createStreamMiddleware()
pipe( pump(
asyncProvider, streamMiddleware.stream,
multiStream.createStream('provider'), mux.createStream('provider'),
asyncProvider, streamMiddleware.stream,
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
) )
// start and stop polling to unblock first block lock
self.idMap = {} // handle sendAsync requests via dapp-side rpc engine
const rpcEngine = new RpcEngine()
rpcEngine.push(createIdRemapMiddleware())
rpcEngine.push(streamMiddleware)
self.rpcEngine = rpcEngine
}
// handle sendAsync requests via asyncProvider // handle sendAsync requests via asyncProvider
self.sendAsync = function (payload, cb) { // also remap ids inbound and outbound
// rewrite request ids MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
var request = eachJsonMessage(payload, (message) => { const self = this
var newId = createRandomId() self.rpcEngine.handle(payload, cb)
self.idMap[newId] = message.id
message.id = newId
return message
})
// forward to asyncProvider
asyncProvider.sendAsync(request, function (err, res) {
if (err) return cb(err)
// transform messages to original ids
eachJsonMessage(res, (message) => {
var oldId = self.idMap[message.id]
delete self.idMap[message.id]
message.id = oldId
return message
})
cb(null, res)
})
}
} }
MetamaskInpageProvider.prototype.send = function (payload) { MetamaskInpageProvider.prototype.send = function (payload) {
const self = this const self = this
@ -80,7 +70,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'eth_coinbase': case 'eth_coinbase':
// read from localStorage // read from localStorage
selectedAddress = self.publicConfigStore.getState().selectedAddress selectedAddress = self.publicConfigStore.getState().selectedAddress
result = selectedAddress result = selectedAddress || null
break break
case 'eth_uninstallFilter': case 'eth_uninstallFilter':
@ -90,7 +80,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'net_version': case 'net_version':
const networkVersion = self.publicConfigStore.getState().networkVersion const networkVersion = self.publicConfigStore.getState().networkVersion
result = networkVersion result = networkVersion || null
break break
// throw not-supported Error // throw not-supported Error
@ -109,10 +99,6 @@ MetamaskInpageProvider.prototype.send = function (payload) {
} }
} }
MetamaskInpageProvider.prototype.sendAsync = function () {
throw new Error('MetamaskInpageProvider - sendAsync not overwritten')
}
MetamaskInpageProvider.prototype.isConnected = function () { MetamaskInpageProvider.prototype.isConnected = function () {
return true return true
} }
@ -121,14 +107,6 @@ MetamaskInpageProvider.prototype.isMetaMask = true
// util // util
function eachJsonMessage (payload, transformFn) {
if (Array.isArray(payload)) {
return payload.map(transformFn)
} else {
return transformFn(payload)
}
}
function logStreamDisconnectWarning (remoteLabel, err) { function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack if (err) warningMsg += '\n' + err.stack

View File

@ -1,13 +1,14 @@
const EthQuery = require('eth-query') const EthQuery = require('ethjs-query')
const assert = require('assert') const assert = require('assert')
const Mutex = require('await-semaphore').Mutex const Mutex = require('await-semaphore').Mutex
class NonceTracker { class NonceTracker {
constructor ({ provider, getPendingTransactions }) { constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider this.provider = provider
this.ethQuery = new EthQuery(provider) this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions
this.lockMap = {} this.lockMap = {}
} }
@ -25,21 +26,28 @@ class NonceTracker {
await this._globalMutexFree() await this._globalMutexFree()
// await lock free, then take lock // await lock free, then take lock
const releaseLock = await this._takeMutex(address) const releaseLock = await this._takeMutex(address)
// calculate next nonce // evaluate multiple nextNonce strategies
// we need to make sure our base count const nonceDetails = {}
// and pending count are from the same block const networkNonceResult = await this._getNetworkNextNonce(address)
const currentBlock = await this._getCurrentBlock() const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const pendingTransactions = this.getPendingTransactions(address) const nextNetworkNonce = networkNonceResult.nonce
const pendingCount = pendingTransactions.length const highestLocalNonce = highestLocallyConfirmed
assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`) const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce)
const baseCountHex = await this._getTxCount(address, currentBlock)
const baseCount = parseInt(baseCountHex, 16) const pendingTxs = this.getPendingTransactions(address)
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
const nextNonce = baseCount + pendingCount
nonceDetails.params = {
highestLocalNonce,
highestSuggested,
nextNetworkNonce,
}
nonceDetails.local = localNonceResult
nonceDetails.network = networkNonceResult
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// collect the numbers used to calculate the nonce for debugging
const blockNumber = currentBlock.number
const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount }
// return nonce and release cb // return nonce and release cb
return { nextNonce, nonceDetails, releaseLock } return { nextNonce, nonceDetails, releaseLock }
} }
@ -53,15 +61,6 @@ class NonceTracker {
}) })
} }
async _getTxCount (address, currentBlock) {
const blockNumber = currentBlock.number
return new Promise((resolve, reject) => {
this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => {
err ? reject(err) : resolve(result)
})
})
}
async _globalMutexFree () { async _globalMutexFree () {
const globalMutex = this._lookupMutex('global') const globalMutex = this._lookupMutex('global')
const release = await globalMutex.acquire() const release = await globalMutex.acquire()
@ -83,12 +82,68 @@ class NonceTracker {
return mutex return mutex
} }
async _getNetworkNextNonce (address) {
// calculate next nonce
// we need to make sure our base count
// and pending count are from the same block
const currentBlock = await this._getCurrentBlock()
const blockNumber = currentBlock.blockNumber
const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
const baseCount = baseCountBN.toNumber()
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
const nonceDetails = { blockNumber, baseCount }
return { name: 'network', nonce: baseCount, details: nonceDetails }
}
_getHighestLocallyConfirmed (address) {
const confirmedTransactions = this.getConfirmedTransactions(address)
const highest = this._getHighestNonce(confirmedTransactions)
return Number.isInteger(highest) ? highest + 1 : 0
}
_reduceTxListToUniqueNonces (txList) {
const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
if (!index) return [txMeta]
const nonceMatches = txList.filter((txData) => {
return txMeta.txParams.nonce === txData.txParams.nonce
})
if (nonceMatches.length > 1) return reducedList
reducedList.push(txMeta)
return reducedList
}, [])
return reducedTxList
}
_getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
const highestNonce = Math.max.apply(null, nonces)
return highestNonce
}
_getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
let highest = startPoint
while (nonces.includes(highest)) {
highest++
}
return { name: 'local', nonce: highest, details: { startPoint, highest } }
}
// this is a hotfix for the fact that the blockTracker will // this is a hotfix for the fact that the blockTracker will
// change when the network changes // change when the network changes
_getBlockTracker () { _getBlockTracker () {
return this.provider._blockTracker return this.provider._blockTracker
} }
} }
module.exports = NonceTracker module.exports = NonceTracker

View File

@ -1,48 +0,0 @@
const through = require('through2')
module.exports = ObjectMultiplex
function ObjectMultiplex (opts) {
opts = opts || {}
// create multiplexer
const mx = through.obj(function (chunk, enc, cb) {
const name = chunk.name
const data = chunk.data
if (!name) {
console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`)
return cb()
}
const substream = mx.streams[name]
if (!substream) {
console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`)
} else {
if (substream.push) substream.push(data)
}
return cb()
})
mx.streams = {}
// create substreams
mx.createStream = function (name) {
const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) {
mx.push({
name: name,
data: chunk,
})
return cb()
})
mx.on('end', function () {
return substream.emit('end')
})
if (opts.error) {
mx.on('error', function () {
return substream.emit('error')
})
}
return substream
}
// ignore streams (dont display orphaned data warning)
mx.ignoreStream = function (name) {
mx.streams[name] = true
}
return mx
}

View File

@ -1,6 +1,7 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const sufficientBalance = require('./util').sufficientBalance const sufficientBalance = require('./util').sufficientBalance
const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
/* /*
Utility class for tracking the transactions as they Utility class for tracking the transactions as they
@ -28,6 +29,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.getBalance = config.getBalance this.getBalance = config.getBalance
this.getPendingTransactions = config.getPendingTransactions this.getPendingTransactions = config.getPendingTransactions
this.publishTransaction = config.publishTransaction this.publishTransaction = config.publishTransaction
this.giveUpOnTransaction = config.giveUpOnTransaction
} }
// checks if a signed tx is in a block and // checks if a signed tx is in a block and
@ -100,6 +102,10 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (balance === undefined) return if (balance === undefined) return
if (!('retryCount' in txMeta)) txMeta.retryCount = 0 if (!('retryCount' in txMeta)) txMeta.retryCount = 0
if (txMeta.retryCount > RETRY_LIMIT) {
return this.giveUpOnTransaction(txMeta.id)
}
// if the value of the transaction is greater then the balance, fail. // if the value of the transaction is greater then the balance, fail.
if (!sufficientBalance(txMeta.txParams, balance)) { if (!sufficientBalance(txMeta.txParams, balance)) {
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')

View File

@ -1,5 +1,6 @@
const Duplex = require('readable-stream').Duplex const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits const inherits = require('util').inherits
const noop = function(){}
module.exports = PortDuplexStream module.exports = PortDuplexStream
@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) {
if (Buffer.isBuffer(msg)) { if (Buffer.isBuffer(msg)) {
delete msg._isBuffer delete msg._isBuffer
var data = new Buffer(msg) var data = new Buffer(msg)
// console.log('PortDuplexStream - saw message as buffer', data)
this.push(data) this.push(data)
} else { } else {
// console.log('PortDuplexStream - saw message', msg)
this.push(msg) this.push(msg)
} }
} }
PortDuplexStream.prototype._onDisconnect = function () { PortDuplexStream.prototype._onDisconnect = function () {
try { this.destroy()
this.push(null)
} catch (err) {
this.emit('error', err)
}
} }
// stream plumbing // stream plumbing
@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
if (Buffer.isBuffer(msg)) { if (Buffer.isBuffer(msg)) {
var data = msg.toJSON() var data = msg.toJSON()
data._isBuffer = true data._isBuffer = true
// console.log('PortDuplexStream - sent message as buffer', data)
this._port.postMessage(data) this._port.postMessage(data)
} else { } else {
// console.log('PortDuplexStream - sent message', msg)
this._port.postMessage(msg) this._port.postMessage(msg)
} }
} catch (err) { } catch (err) {
// console.error(err)
return cb(new Error('PortDuplexStream - disconnected')) return cb(new Error('PortDuplexStream - disconnected'))
} }
cb() cb()
} }
// util
function noop () {}

View File

@ -1,6 +1,6 @@
const Through = require('through2') const Through = require('through2')
const endOfStream = require('end-of-stream') const ObjectMultiplex = require('obj-multiplex')
const ObjectMultiplex = require('./obj-multiplex') const pump = require('pump')
module.exports = { module.exports = {
jsonParseStream: jsonParseStream, jsonParseStream: jsonParseStream,
@ -23,14 +23,14 @@ function jsonStringifyStream () {
} }
function setupMultiplex (connectionStream) { function setupMultiplex (connectionStream) {
var mx = ObjectMultiplex() const mux = new ObjectMultiplex()
connectionStream.pipe(mx).pipe(connectionStream) pump(
endOfStream(mx, function (err) { connectionStream,
mux,
connectionStream,
(err) => {
if (err) console.error(err) if (err) console.error(err)
}) }
endOfStream(connectionStream, function (err) { )
if (err) console.error(err) return mux
mx.destroy()
})
return mx
} }

View File

@ -0,0 +1,37 @@
const jsonDiffer = require('fast-json-patch')
const clone = require('clone')
module.exports = {
generateHistoryEntry,
replayHistory,
snapshotFromTxMeta,
migrateFromSnapshotsToDiffs,
}
function migrateFromSnapshotsToDiffs(longHistory) {
return (
longHistory
// convert non-initial history entries into diffs
.map((entry, index) => {
if (index === 0) return entry
return generateHistoryEntry(longHistory[index - 1], entry)
})
)
}
function generateHistoryEntry(previousState, newState) {
return jsonDiffer.compare(previousState, newState)
}
function replayHistory(shortHistory) {
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
}
function snapshotFromTxMeta(txMeta) {
// create txMeta snapshot for history
const snapshot = clone(txMeta)
// dont include previous history in this snapshot
delete snapshot.history
return snapshot
}

View File

@ -1,12 +1,18 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const extend = require('xtend') const extend = require('xtend')
const promiseToCallback = require('promise-to-callback') const promiseToCallback = require('promise-to-callback')
const pipe = require('pump') const pump = require('pump')
const Dnode = require('dnode') const Dnode = require('dnode')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const EthStore = require('./lib/eth-store') const EthStore = require('./lib/eth-store')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const streamIntoProvider = require('web3-stream-provider/handler') const RpcEngine = require('json-rpc-engine')
const debounce = require('debounce')
const createEngineStream = require('json-rpc-middleware-stream/engineStream')
const createFilterMiddleware = require('eth-json-rpc-filters')
const createOriginMiddleware = require('./lib/createOriginMiddleware')
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const createProviderMiddleware = require('./lib/createProviderMiddleware')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const KeyringController = require('./keyring-controller') const KeyringController = require('./keyring-controller')
const NetworkController = require('./controllers/network') const NetworkController = require('./controllers/network')
@ -24,8 +30,6 @@ const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify') const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies') const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url') const getBuyEthUrl = require('./lib/buy-eth-url')
const debounce = require('debounce')
const version = require('../manifest.json').version const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
@ -77,12 +81,13 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
this.blockTracker = this.provider
// eth data query tools // eth data query tools
this.ethQuery = new EthQuery(this.provider) this.ethQuery = new EthQuery(this.provider)
this.ethStore = new EthStore({ this.ethStore = new EthStore({
provider: this.provider, provider: this.provider,
blockTracker: this.provider, blockTracker: this.blockTracker,
}) })
// key mgmt // key mgmt
@ -109,7 +114,7 @@ module.exports = class MetamaskController extends EventEmitter {
getNetwork: this.networkController.getNetworkState.bind(this), getNetwork: this.networkController.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController), signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider, provider: this.provider,
blockTracker: this.provider, blockTracker: this.blockTracker,
ethQuery: this.ethQuery, ethQuery: this.ethQuery,
ethStore: this.ethStore, ethStore: this.ethStore,
}) })
@ -337,36 +342,43 @@ module.exports = class MetamaskController extends EventEmitter {
setupUntrustedCommunication (connectionStream, originDomain) { setupUntrustedCommunication (connectionStream, originDomain) {
// Check if new connection is blacklisted // Check if new connection is blacklisted
if (this.blacklistController.checkForPhishing(originDomain)) { if (this.blacklistController.checkForPhishing(originDomain)) {
console.log('MetaMask - sending phishing warning for', originDomain) log.debug('MetaMask - sending phishing warning for', originDomain)
this.sendPhishingWarning(connectionStream, originDomain) this.sendPhishingWarning(connectionStream, originDomain)
return return
} }
// setup multiplexing // setup multiplexing
const mx = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
// connect features // connect features
this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain)
this.setupPublicConfig(mx.createStream('publicConfig')) this.setupPublicConfig(mux.createStream('publicConfig'))
} }
setupTrustedCommunication (connectionStream, originDomain) { setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing // setup multiplexing
const mx = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
// connect features // connect features
this.setupControllerConnection(mx.createStream('controller')) this.setupControllerConnection(mux.createStream('controller'))
this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain)
} }
sendPhishingWarning (connectionStream, hostname) { sendPhishingWarning (connectionStream, hostname) {
const mx = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
const phishingStream = mx.createStream('phishing') const phishingStream = mux.createStream('phishing')
phishingStream.write({ hostname }) phishingStream.write({ hostname })
} }
setupControllerConnection (outStream) { setupControllerConnection (outStream) {
const api = this.getApi() const api = this.getApi()
const dnode = Dnode(api) const dnode = Dnode(api)
outStream.pipe(dnode).pipe(outStream) pump(
outStream,
dnode,
outStream,
(err) => {
if (err) log.error(err)
}
)
dnode.on('remote', (remote) => { dnode.on('remote', (remote) => {
// push updates to popup // push updates to popup
const sendUpdate = remote.sendUpdate.bind(remote) const sendUpdate = remote.sendUpdate.bind(remote)
@ -374,27 +386,42 @@ module.exports = class MetamaskController extends EventEmitter {
}) })
} }
setupProviderConnection (outStream, originDomain) { setupProviderConnection (outStream, origin) {
streamIntoProvider(outStream, this.provider, onRequest, onResponse) // setup json rpc engine stack
// append dapp origin domain to request const engine = new RpcEngine()
function onRequest (request) {
request.origin = originDomain // create filter polyfill middleware
} const filterMiddleware = createFilterMiddleware({
// log rpc activity provider: this.provider,
function onResponse (err, request, response) { blockTracker: this.blockTracker,
if (err) return console.error(err) })
if (response.error) {
console.error('Error in RPC response:\n', response) engine.push(createOriginMiddleware({ origin }))
} engine.push(createLoggerMiddleware({ origin }))
if (request.isMetamaskInternal) return engine.push(filterMiddleware)
log.info(`RPC (${originDomain}):`, request, '->', response) engine.push(createProviderMiddleware({ provider: this.provider }))
// setup connection
const providerStream = createEngineStream({ engine })
pump(
outStream,
providerStream,
outStream,
(err) => {
// cleanup filter polyfill middleware
filterMiddleware.destroy()
if (err) log.error(err)
} }
)
} }
setupPublicConfig (outStream) { setupPublicConfig (outStream) {
pipe( pump(
this.publicConfigStore, this.publicConfigStore,
outStream outStream,
(err) => {
if (err) log.error(err)
}
) )
} }

View File

@ -0,0 +1,52 @@
const version = 18
/*
This migration updates "transaction state history" to diffs style
*/
const clone = require('clone')
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
// no history: initialize
if (!txMeta.history || txMeta.history.length === 0) {
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history = [snapshot]
return txMeta
}
// has history: migrate
const newHistory = (
txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history)
// remove empty diffs
.filter((entry) => {
return !Array.isArray(entry) || entry.length > 0
})
)
txMeta.history = newHistory
return txMeta
})
return newState
}

View File

@ -0,0 +1,83 @@
const version = 19
/*
This migration sets transactions as failed
whos nonce is too high
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
if (txMeta.status !== 'submitted') return txMeta
const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestConfirmedNonce = getHighestNonce(confirmedTxs)
const pendingTxs = txList.filter((tx) => tx.status === 'submitted')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce)
const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce)
if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) {
txMeta.status = 'failed'
txMeta.err = {
message: 'nonce too high',
note: 'migration 019 custom error',
}
}
return txMeta
})
return newState
}
function getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
return parseInt(nonce, 16)
})
let highest = startPoint
while (nonces.includes(highest)) {
highest++
}
return highest
}
function getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
return parseInt(nonce || '0x0', 16)
})
const highestNonce = Math.max.apply(null, nonces)
return highestNonce
}

View File

@ -28,4 +28,6 @@ module.exports = [
require('./015'), require('./015'),
require('./016'), require('./016'),
require('./017'), require('./017'),
require('./018'),
require('./019'),
] ]

View File

@ -1,10 +1,17 @@
machine: machine:
node: node:
version: 8.1.4 version: 8.1.4
dependencies:
pre:
- "npm i -g testem"
- "npm i -g mocha"
test: test:
override: override:
- "npm run ci" - "npm run ci"
dependencies:
pre:
- sudo apt-get update
# get latest stable firefox
- sudo apt-get install firefox
- firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd
# get latest stable chrome
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
- sudo apt-get update
- sudo apt-get install google-chrome-stable

View File

@ -1,7 +1,7 @@
const createStore = require('redux').createStore const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk') const thunkMiddleware = require('redux-thunk').default
const createLogger = require('redux-logger') const createLogger = require('redux-logger').createLogger
const rootReducer = require('../ui/app/reducers') const rootReducer = require('../ui/app/reducers')
module.exports = configureStore module.exports = configureStore

61
karma.conf.js Normal file
View File

@ -0,0 +1,61 @@
// Karma configuration
// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: process.cwd(),
browserConsoleLogOptions: {
terminal: false,
},
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['qunit'],
// list of files / patterns to load in the browser
files: [
'development/bundle.js',
'test/integration/jquery-3.1.0.min.js',
'test/integration/bundle.js',
{ pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true },
{ pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true },
],
proxies: {
'/images/': '/base/dist/chrome/images/',
'/fonts/': '/base/dist/chrome/fonts/',
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome', 'Firefox'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}

View File

@ -1,5 +1,5 @@
const Iframe = require('iframe') const Iframe = require('iframe')
const IframeStream = require('iframe-stream').IframeStream const createIframeStream = require('iframe-stream').IframeStream
module.exports = setupIframe module.exports = setupIframe
@ -13,7 +13,7 @@ function setupIframe(opts) {
}) })
var iframe = frame.iframe var iframe = frame.iframe
iframe.style.setProperty('display', 'none') iframe.style.setProperty('display', 'none')
var iframeStream = new IframeStream(iframe) var iframeStream = createIframeStream(iframe)
return iframeStream return iframeStream
} }

View File

@ -1,4 +1,4 @@
const ParentStream = require('iframe-stream').ParentStream const createParentStream = require('iframe-stream').ParentStream
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js') const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js') const SwStream = require('sw-stream/lib/sw-stream.js')
const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js') const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js')
@ -11,7 +11,7 @@ const background = new SWcontroller({
intervalDelay, intervalDelay,
}) })
const pageStream = new ParentStream() const pageStream = createParentStream()
background.on('ready', (_) => { background.on('ready', (_) => {
let swStream = SwStream({ let swStream = SwStream({
serviceWorker: background.controller, serviceWorker: background.controller,

View File

@ -85,12 +85,19 @@ actions.update = function(stateName) {
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
const container = document.querySelector('#app-content')
// parse opts // parse opts
var store = configureStore(firstState) var store = configureStore(firstState)
// start app // start app
startApp()
function startApp(){
const body = document.body
const container = document.createElement('div')
container.id = 'app-content'
body.appendChild(container)
console.log('container', container)
render( render(
h('.super-dev-container', [ h('.super-dev-container', [
@ -121,4 +128,4 @@ render(
] ]
), container) ), container)
}

View File

@ -12,8 +12,8 @@
"test": "npm run lint && npm run test-unit && npm run test-integration", "test": "npm run lint && npm run test-unit && npm run test-integration",
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"single-test": "METAMASK_ENV=test mocha --require test/helper.js", "single-test": "METAMASK_ENV=test mocha --require test/helper.js",
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "test-integration": "npm run buildMock && npm run buildCiUnits && karma start",
"test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls", "test-coverage": "nyc npm run test-unit && if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"ci": "npm run lint && npm run test-coverage && npm run test-integration", "ci": "npm run lint && npm run test-coverage && npm run test-integration",
"lint": "gulp lint", "lint": "gulp lint",
"buildCiUnits": "node test/integration/index.js", "buildCiUnits": "node test/integration/index.js",
@ -22,7 +22,6 @@
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js", "buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
"testem": "npm run buildMock && testem",
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"generateNotice": "node notices/notice-generator.js", "generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js", "deleteNotice": "node notices/notice-delete.js",
@ -74,11 +73,12 @@
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-contract-metadata": "^1.1.4", "eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.1.1", "eth-hd-keyring": "^1.1.1",
"eth-json-rpc-filters": "^1.1.0",
"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.2.2", "eth-sig-util": "^1.2.2",
"eth-simple-keyring": "^1.1.1", "eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.1.2", "eth-token-tracker": "^1.1.3",
"ethereumjs-tx": "^1.3.0", "ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
@ -88,6 +88,7 @@
"express": "^4.14.0", "express": "^4.14.0",
"extension-link-enabler": "^1.0.0", "extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0", "extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"gulp": "github:gulpjs/gulp#4.0", "gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^4.0.0", "gulp-autoprefixer": "^4.0.0",
@ -101,12 +102,15 @@
"iframe-stream": "^3.0.0", "iframe-stream": "^3.0.0",
"inject-css": "^0.1.1", "inject-css": "^0.1.1",
"jazzicon": "^1.2.0", "jazzicon": "^1.2.0",
"json-rpc-engine": "^3.1.0",
"json-rpc-middleware-stream": "^1.0.0",
"loglevel": "^1.4.1", "loglevel": "^1.4.1",
"metamask-logo": "^2.1.2", "metamask-logo": "^2.1.2",
"mississippi": "^1.2.0", "mississippi": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"multiplex": "^6.7.0", "multiplex": "^6.7.0",
"number-to-bn": "^1.7.0", "number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
"obs-store": "^2.3.1", "obs-store": "^2.3.1",
"once": "^1.3.3", "once": "^1.3.3",
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
@ -130,7 +134,7 @@
"react-tooltip-component": "^0.3.0", "react-tooltip-component": "^0.3.0",
"react-transition-group": "^2.2.0", "react-transition-group": "^2.2.0",
"reactify": "^1.1.1", "reactify": "^1.1.1",
"readable-stream": "^2.1.2", "readable-stream": "^2.3.3",
"redux": "^3.0.5", "redux": "^3.0.5",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
@ -150,7 +154,7 @@
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.24.1", "babel-core": "^6.24.1",
"babel-eslint": "^7.2.3", "babel-eslint": "^8.0.0",
"babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0", "babel-polyfill": "^6.23.0",
@ -186,6 +190,11 @@
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1", "jshint-stylish": "~2.2.1",
"json-rpc-engine": "^3.0.1", "json-rpc-engine": "^3.0.1",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1",
"karma-firefox-launcher": "^1.0.1",
"karma-qunit": "^1.2.1",
"lodash.assign": "^4.0.6", "lodash.assign": "^4.0.6",
"mocha": "^3.4.2", "mocha": "^3.4.2",
"mocha-eslint": "^4.0.0", "mocha-eslint": "^4.0.0",
@ -200,8 +209,8 @@
"react-addons-test-utils": "^15.5.1", "react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.5.4", "react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"sinon": "^2.3.8",
"stylelint-config-standard": "^17.0.0", "stylelint-config-standard": "^17.0.0",
"sinon": "^3.2.0",
"tape": "^4.5.1", "tape": "^4.5.1",
"testem": "^1.10.3", "testem": "^1.10.3",
"uglifyify": "^4.0.2", "uglifyify": "^4.0.2",

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
function wait(time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
})
}

View File

@ -1,5 +1,6 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const pump = require('pump')
const browserify = require('browserify') const browserify = require('browserify')
const tests = fs.readdirSync(path.join(__dirname, 'lib')) const tests = fs.readdirSync(path.join(__dirname, 'lib'))
const bundlePath = path.join(__dirname, 'bundle.js') const bundlePath = path.join(__dirname, 'bundle.js')
@ -9,11 +10,17 @@ const b = browserify()
const writeStream = fs.createWriteStream(bundlePath) const writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function (fileName) { tests.forEach(function (fileName) {
b.add(path.join(__dirname, 'lib', fileName)) const filePath = path.join(__dirname, 'lib', fileName)
console.log(`bundling test "${filePath}"`)
b.add(filePath)
}) })
b.bundle() pump(
.pipe(writeStream) b.bundle(),
.on('error', (err) => { writeStream,
throw err (err) => {
}) if (err) throw err
console.log(`Integration test build completed: "${bundlePath}"`)
process.exit(0)
}
)

View File

@ -2,125 +2,137 @@ const PASSWORD = 'password123'
QUnit.module('first time usage') QUnit.module('first time usage')
QUnit.test('render init screen', function (assert) { QUnit.test('render init screen', (assert) => {
var done = assert.async() const done = assert.async()
let app runFirstTimeUsageTest(assert).then(done).catch((err) => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
wait().then(function() {
app = $('iframe').contents().find('#app-content .mock-app-root')
const recurseNotices = function () {
let button = app.find('button')
if (button.html() === 'Accept') {
let termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
return wait().then(() => {
button.click()
return wait()
}).then(() => {
return recurseNotices()
})
} else {
return wait()
}
}
return recurseNotices()
}).then(function() {
// Scroll through terms
var title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
// enter password
var pwBox = app.find('#password-box')[0]
var confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
return wait()
}).then(function() {
// create vault
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1500)
}).then(function() {
var created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button
var button = app.find('button')[0]
assert.ok(button, 'button present')
button.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
var sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
return wait()
}).then(function() {
var sandwich = app.find('.menu-droppo')[0]
var children = sandwich.children
var lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
return wait(1000)
}).then(function() {
var pwBox = app.find('#password-box')[0]
pwBox.value = PASSWORD
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded again.')
return wait()
}).then(function (){
var qrButton = app.find('.fa.fa-ellipsis-h')[0] // open account settings dropdown
qrButton.click()
return wait(1000)
}).then(function (){
var qrButton = app.find('.dropdown-menu-item')[1] // qr code item
qrButton.click()
return wait(1000)
}).then(function (){
var qrHeader = app.find('.qr-header')[0]
var qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
networkMenu.click()
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
var children = networkMenu.children
children.length[3]
assert.ok(children, 'All network options present')
done() done()
}) })
}) })
// QUnit.testDone(({ module, name, total, passed, failed, skipped, todo, runtime }) => {
// if (failed > 0) {
// const app = $('iframe').contents()[0].documentElement
// console.warn('Test failures - dumping DOM:')
// console.log(app.innerHTML)
// }
// })
async function runFirstTimeUsageTest(assert, done) {
await timeout()
const app = $('#app-content .mock-app-root')
// recurse notices
while (true) {
const button = app.find('button')
if (button.html() === 'Accept') {
// still notices to accept
const termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
await timeout()
button.click()
await timeout()
} else {
// exit loop
break
}
}
await timeout()
// Scroll through terms
const title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
// enter password
const pwBox = app.find('#password-box')[0]
const confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
await timeout()
// create vault
const createButton = app.find('button.primary')[0]
createButton.click()
await timeout(1500)
const created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button
const button = app.find('button')[0]
assert.ok(button, 'button present')
button.click()
await timeout(1000)
const detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
const sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
await timeout()
const menu = app.find('.menu-droppo')[0]
const children = menu.children
const lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
await timeout(1000)
const pwBox2 = app.find('#password-box')[0]
pwBox2.value = PASSWORD
const createButton2 = app.find('button.primary')[0]
createButton2.click()
await timeout(1000)
const detail2 = app.find('.account-detail-section')[0]
assert.ok(detail2, 'Account detail section loaded again.')
await timeout()
// open account settings dropdown
const qrButton = app.find('.fa.fa-ellipsis-h')[0]
qrButton.click()
await timeout(1000)
// qr code item
const qrButton2 = app.find('.dropdown-menu-item')[1]
qrButton2.click()
await timeout(1000)
const qrHeader = app.find('.qr-header')[0]
const qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
await timeout()
const networkMenu = app.find('.network-indicator')[0]
networkMenu.click()
await timeout()
const networkMenu2 = app.find('.network-indicator')[0]
const children2 = networkMenu2.children
children2.length[3]
assert.ok(children2, 'All network options present')
}
function timeout(time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
})
}

View File

@ -1,7 +1,7 @@
const createStore = require('redux').createStore const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk') const thunkMiddleware = require('redux-thunk').default
const createLogger = require('redux-logger') const createLogger = require('redux-logger').createLogger
const rootReducer = function () {} const rootReducer = function () {}
module.exports = configureStore module.exports = configureStore

40
test/lib/mock-tx-gen.js Normal file
View File

@ -0,0 +1,40 @@
const extend = require('xtend')
const BN = require('ethereumjs-util').BN
const template = {
'status': 'submitted',
'txParams': {
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
'gas': '0x30d40',
'value': '0x0',
'nonce': '0x3',
},
}
class TxGenerator {
constructor () {
this.txs = []
}
generate (tx = {}, opts = {}) {
let { count, fromNonce } = opts
let nonce = fromNonce || this.txs.length
let txs = []
for (let i = 0; i < count; i++) {
txs.push(extend(template, {
txParams: {
nonce: hexify(nonce++),
}
}, tx))
}
this.txs = this.txs.concat(txs)
return txs
}
}
function hexify (number) {
return '0x' + (new BN(number)).toString(16)
}
module.exports = TxGenerator

View File

@ -65,91 +65,6 @@ describe('tx confirmation screen', function () {
assert.equal(count, 0) assert.equal(count, 0)
}) })
}) })
describe('sendTx', function () {
var result
describe('when there is an error', function () {
before(function (done) {
actions._setBackgroundConnection({
approveTransaction (txId, cb) { cb({message: 'An error!'}) },
})
actions.sendTx({id: firstTxId})(function (action) {
result = reducers(initialState, action)
done()
})
})
it('should stay on the page', function () {
assert.equal(result.appState.currentView.name, 'confTx')
})
it('should set errorMessage on the currentView', function () {
assert(result.appState.currentView.errorMessage)
})
})
describe('when there is success', function () {
it('should complete tx and go home', function () {
actions._setBackgroundConnection({
approveTransaction (txId, cb) { cb() },
})
var dispatchExpect = sinon.mock()
dispatchExpect.twice()
actions.sendTx({id: firstTxId})(dispatchExpect)
})
})
})
describe('when there are two pending txs', function () {
var firstTxId = 1457634084250832
var result, initialState
before(function (done) {
initialState = {
appState: {
currentView: {
name: 'confTx',
},
},
metamask: {
unapprovedTxs: {
'1457634084250832': {
id: firstTxId,
status: 'unconfirmed',
time: 1457634084250,
},
'1457634084250833': {
id: 1457634084250833,
status: 'unconfirmed',
time: 1457634084255,
},
},
},
}
freeze(initialState)
// Mocking a background connection:
actions._setBackgroundConnection({
approveTransaction (firstTxId, cb) { cb() },
})
actions.sendTx({id: firstTxId})(function (action) {
result = reducers(initialState, action)
})
done()
})
it('should stay on the confTx view', function () {
assert.equal(result.appState.currentView.name, 'confTx')
})
it('should transition to the first tx', function () {
assert.equal(result.appState.currentView.context, 0)
})
})
}) })
}) })

View File

@ -1,41 +1,203 @@
const assert = require('assert') const assert = require('assert')
const NonceTracker = require('../../app/scripts/lib/nonce-tracker') const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
let providerResultStub = {}
describe('Nonce Tracker', function () { describe('Nonce Tracker', function () {
let nonceTracker, provider, getPendingTransactions, pendingTxs let nonceTracker, provider
let getPendingTransactions, pendingTxs
let getConfirmedTransactions, confirmedTxs
beforeEach(function () {
pendingTxs = [{
'status': 'submitted',
'txParams': {
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
'gas': '0x30d40',
'value': '0x0',
'nonce': '0x0',
},
}]
getPendingTransactions = () => pendingTxs
provider = {
sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) },
_blockTracker: {
getCurrentBlock: () => '0x11b568',
},
}
nonceTracker = new NonceTracker({
provider,
getPendingTransactions,
})
})
describe('#getNonceLock', function () { describe('#getNonceLock', function () {
it('should work', async function () {
describe('with 3 confirmed and 1 pending', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
})
it('should return 4', async function () {
this.timeout(15000) this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1') assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
await nonceLock.releaseLock()
})
})
describe('with no previous txs', function () {
beforeEach(function () {
nonceTracker = generateNonceTrackerWith([], [])
})
it('should return 0', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('with multiple previous txs with same nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 })
pendingTxs = txGen.generate({
status: 'submitted',
txParams: { nonce: '0x01' },
}, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local confirmed count is higher than network nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local pending count is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [])
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when provider nonce is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are some pending nonces below the remote one and some over.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are pending nonces non sequentially over the network nonce.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
txGen.generate({ status: 'submitted' }, { count: 5 })
// 5 over that number
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('When all three return different values', function () {
beforeEach(function () {
const txGen = new MockTxGen()
const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 })
const pendingTxs = txGen.generate({
status: 'submitted',
nonce: 100,
}, { count: 1 })
// 0x32 is 50 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('Faq issue 67', function () {
beforeEach(function () {
const txGen = new MockTxGen()
const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 })
const pendingTxs = txGen.generate({
status: 'submitted',
}, { count: 10 })
// 0x40 is 64 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x40')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '74', `nonce should be 74 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock() await nonceLock.releaseLock()
}) })
}) })
}) })
})
function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
const getPendingTransactions = () => pending
const getConfirmedTransactions = () => confirmed
providerResultStub.result = providerStub
const provider = {
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
_blockTracker: {
getCurrentBlock: () => '0x11b568',
},
}
return new NonceTracker({
provider,
getPendingTransactions,
getConfirmedTransactions,
})
}

View File

@ -6,12 +6,15 @@ const clone = require('clone')
const sinon = require('sinon') const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions') const TransactionController = require('../../app/scripts/controllers/transactions')
const TxProvideUtils = require('../../app/scripts/lib/tx-utils') const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const noop = () => true const noop = () => true
const currentNetworkId = 42 const currentNetworkId = 42
const otherNetworkId = 36 const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
const { createStubedProvider } = require('../stub/provider') const { createStubedProvider } = require('../stub/provider')
describe('Transaction Controller', function () { describe('Transaction Controller', function () {
let txController, engine, provider, providerResultStub let txController, engine, provider, providerResultStub
@ -47,7 +50,7 @@ describe('Transaction Controller', function () {
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams, txParams,
} }
txController._saveTxList([txMeta]) txController.addTx(txMeta)
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta)) stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta))
}) })
@ -279,12 +282,15 @@ describe('Transaction Controller', function () {
it('replaces the tx with the same id', function () { it('replaces the tx with the same id', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} }) const tx1 = txController.getTx('1')
var result = txController.getTx('1') tx1.status = 'blah'
assert.equal(result.hash, 'foo') tx1.hash = 'foo'
txController.updateTx(tx1)
const savedResult = txController.getTx('1')
assert.equal(savedResult.hash, 'foo')
}) })
it('updates gas price', function () { it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01' const originalGasPrice = '0x01'
const desiredGasPrice = '0x02' const desiredGasPrice = '0x02'
@ -297,13 +303,22 @@ describe('Transaction Controller', function () {
}, },
} }
const updatedMeta = clone(txMeta)
txController.addTx(txMeta) txController.addTx(txMeta)
updatedMeta.txParams.gasPrice = desiredGasPrice const updatedTx = txController.getTx('1')
txController.updateTx(updatedMeta) // verify tx was initialized correctly
var result = txController.getTx('1') assert.equal(updatedTx.history.length, 1, 'one history item (initial)')
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state')
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
// modify value and updateTx
updatedTx.txParams.gasPrice = desiredGasPrice
txController.updateTx(updatedTx)
// check updated value
const result = txController.getTx('1')
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated') assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
// validate history was updated
assert.equal(result.history.length, 2, 'two history items (initial + diff)')
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)')
}) })
}) })

View File

@ -0,0 +1,26 @@
const assert = require('assert')
const clone = require('clone')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
describe('deepCloneFromTxMeta', function () {
it('should clone deep', function () {
const input = {
foo: {
bar: {
bam: 'baz'
}
}
}
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
assert('foo' in output, 'has a foo key')
assert('bar' in output.foo, 'has a bar key')
assert('bam' in output.foo.bar, 'has a bar key')
assert.equal(output.foo.bar.bam, 'baz', 'has a baz value')
})
it('should remove the history key', function () {
const input = { foo: 'bar', history: 'remembered' }
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
assert(typeof output.history, 'undefined', 'should remove history')
})
})

View File

@ -0,0 +1,23 @@
const assert = require('assert')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json')
describe('tx-state-history-helper', function () {
it('migrates history to diffs and can recover original values', function () {
testVault.data.TransactionController.transactions.forEach((tx, index) => {
const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history)
newHistory.forEach((newEntry, index) => {
if (index === 0) {
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj')
} else {
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj')
}
const oldEntry = tx.history[index]
const historySubset = newHistory.slice(0, index + 1)
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset)
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs')
})
})
})
})

View File

@ -1,10 +0,0 @@
launch_in_dev:
- Chrome
- Firefox
launch_in_ci:
- Chrome
- Firefox
framework:
- qunit
before_tests: "npm run buildCiUnits"
test_page: "test/integration/index.html"

View File

@ -126,6 +126,7 @@ var actions = {
txError: txError, txError: txError,
nextTx: nextTx, nextTx: nextTx,
previousTx: previousTx, previousTx: previousTx,
cancelAllTx: cancelAllTx,
viewPendingTx: viewPendingTx, viewPendingTx: viewPendingTx,
VIEW_PENDING_TX: 'VIEW_PENDING_TX', VIEW_PENDING_TX: 'VIEW_PENDING_TX',
// app messages // app messages
@ -420,6 +421,7 @@ function signPersonalMsg (msgData) {
function signTx (txData) { function signTx (txData) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication())
global.ethQuery.sendTransaction(txData, (err, data) => { global.ethQuery.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message)) if (err) return dispatch(actions.displayWarning(err.message))
@ -464,6 +466,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
dispatch(actions.txError(err)) dispatch(actions.txError(err))
dispatch(actions.goHome())
return log.error(err.message) return log.error(err.message)
} }
dispatch(actions.completedTx(txData.id)) dispatch(actions.completedTx(txData.id))
@ -506,6 +509,16 @@ function cancelTx (txData) {
} }
} }
function cancelAllTx (txsData) {
return (dispatch) => {
txsData.forEach((txData, i) => {
background.cancelTransaction(txData.id, () => {
dispatch(actions.completedTx(txData.id))
i === txsData.length - 1 ? dispatch(actions.goHome()) : null
})
})
}
}
// //
// initialize screen // initialize screen
// //

View File

@ -3,6 +3,8 @@ const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const Tooltip = require('./components/tooltip.js')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const abi = require('human-standard-token-abi') const abi = require('human-standard-token-abi')
@ -15,6 +17,7 @@ module.exports = connect(mapStateToProps)(AddTokenScreen)
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
identities: state.metamask.identities,
} }
} }
@ -64,15 +67,25 @@ AddTokenScreen.prototype.render = function () {
}, [ }, [
h('div', [ h('div', [
h('span', { h(Tooltip, {
position: 'top',
title: 'The contract of the actual token contract. Click for more info.',
}, [
h('a', {
style: { fontWeight: 'bold', paddingRight: '10px'}, style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Address'), href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address',
target: '_blank',
}, [
h('span', 'Token Contract Address '),
h('i.fa.fa-question-circle'),
]),
]),
]), ]),
h('section.flex-row.flex-center', [ h('section.flex-row.flex-center', [
h('input#token-address', { h('input#token-address', {
name: 'address', name: 'address',
placeholder: 'Token Address', placeholder: 'Token Contract Address',
onChange: this.tokenAddressDidChange.bind(this), onChange: this.tokenAddressDidChange.bind(this),
style: { style: {
width: 'inherit', width: 'inherit',
@ -171,7 +184,9 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (event) {
AddTokenScreen.prototype.validateInputs = function () { AddTokenScreen.prototype.validateInputs = function () {
let msg = '' let msg = ''
const state = this.state const state = this.state
const identitiesList = Object.keys(this.props.identities)
const { address, symbol, decimals } = state const { address, symbol, decimals } = state
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
const validAddress = ethUtil.isValidAddress(address) const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) { if (!validAddress) {
@ -189,7 +204,12 @@ AddTokenScreen.prototype.validateInputs = function () {
msg += 'Symbol must be between 0 and 10 characters.' msg += 'Symbol must be between 0 and 10 characters.'
} }
const isValid = validAddress && validDecimals const ownAddress = identitiesList.includes(standardAddress)
if (ownAddress) {
msg = 'Personal address detected. Input the token contract address.'
}
const isValid = validAddress && validDecimals && !ownAddress
if (!isValid) { if (!isValid) {
this.setState({ this.setState({
@ -215,4 +235,3 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
} }
} }

View File

@ -46,6 +46,7 @@ function mapStateToProps (state) {
identities, identities,
accounts, accounts,
address, address,
keyrings,
} = state.metamask } = state.metamask
const selected = address || Object.keys(accounts)[0] const selected = address || Object.keys(accounts)[0]
@ -75,6 +76,7 @@ function mapStateToProps (state) {
// state needed to get account dropdown temporarily rendering from app bar // state needed to get account dropdown temporarily rendering from app bar
identities, identities,
selected, selected,
keyrings,
} }
} }

View File

@ -1,6 +1,7 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const exportAsFile = require('../util').exportAsFile
const copyToClipboard = require('copy-to-clipboard') const copyToClipboard = require('copy-to-clipboard')
const actions = require('../actions') const actions = require('../actions')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
@ -20,20 +21,21 @@ function mapStateToProps (state) {
} }
ExportAccountView.prototype.render = function () { ExportAccountView.prototype.render = function () {
var state = this.props const state = this.props
var accountDetail = state.accountDetail const accountDetail = state.accountDetail
const nickname = state.identities[state.address].name
if (!accountDetail) return h('div') if (!accountDetail) return h('div')
var accountExport = accountDetail.accountExport const accountExport = accountDetail.accountExport
var notExporting = accountExport === 'none' const notExporting = accountExport === 'none'
var exportRequested = accountExport === 'requested' const exportRequested = accountExport === 'requested'
var accountExported = accountExport === 'completed' const accountExported = accountExport === 'completed'
if (notExporting) return h('div') if (notExporting) return h('div')
if (exportRequested) { if (exportRequested) {
var warning = `Export private keys at your own risk.` const warning = `Export private keys at your own risk.`
return ( return (
h('div', { h('div', {
style: { style: {
@ -89,6 +91,8 @@ ExportAccountView.prototype.render = function () {
} }
if (accountExported) { if (accountExported) {
const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
return h('div.privateKey', { return h('div.privateKey', {
style: { style: {
margin: '0 20px', margin: '0 20px',
@ -105,10 +109,16 @@ ExportAccountView.prototype.render = function () {
onClick: function (event) { onClick: function (event) {
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
}, },
}, ethUtil.stripHexPrefix(accountDetail.privateKey)), }, plainKey),
h('button', { h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
}, 'Done'), }, 'Done'),
h('button', {
style: {
marginLeft: '10px',
},
onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey),
}, 'Save as File'),
]) ])
} }
} }
@ -117,6 +127,6 @@ ExportAccountView.prototype.onExportKeyPress = function (event) {
if (event.key !== 'Enter') return if (event.key !== 'Enter') return
event.preventDefault() event.preventDefault()
var input = document.getElementById('exportAccount').value const input = document.getElementById('exportAccount').value
this.props.dispatch(actions.exportAccount(input, this.props.address)) this.props.dispatch(actions.exportAccount(input, this.props.address))
} }

View File

@ -25,7 +25,7 @@ class AccountDropdowns extends Component {
} }
renderAccounts () { renderAccounts () {
const { identities, accounts, selected, menuItemStyles, actions } = this.props const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props
return Object.keys(identities).map((key, index) => { return Object.keys(identities).map((key, index) => {
const identity = identities[key] const identity = identities[key]
@ -33,6 +33,12 @@ class AccountDropdowns extends Component {
const balanceValue = accounts[key].balance const balanceValue = accounts[key].balance
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
})
return h( return h(
DropdownMenuItem, DropdownMenuItem,
@ -88,6 +94,7 @@ class AccountDropdowns extends Component {
marginLeft: '10px', marginLeft: '10px',
}, },
}, [ }, [
this.indicateIfLoose(keyring),
h('span.account-dropdown-name', { h('span.account-dropdown-name', {
style: { style: {
fontSize: '18px', fontSize: '18px',
@ -97,6 +104,7 @@ class AccountDropdowns extends Component {
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
}, },
}, identity.name || ''), }, identity.name || ''),
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
h('span.account-dropdown-balance', { h('span.account-dropdown-balance', {
style: { style: {
@ -125,11 +133,35 @@ class AccountDropdowns extends Component {
]), ]),
]), ]),
// =======
// },
// ),
// this.indicateIfLoose(keyring),
// h('span', {
// style: {
// marginLeft: '20px',
// fontSize: '24px',
// maxWidth: '145px',
// whiteSpace: 'nowrap',
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// },
// }, identity.name || ''),
// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
// >>>>>>> master:ui/app/components/account-dropdowns.js
] ]
) )
}) })
} }
indicateIfLoose (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
} catch (e) { return }
}
renderAccountSelector () { renderAccountSelector () {
const { actions, useCssTransition, innerStyle } = this.props const { actions, useCssTransition, innerStyle } = this.props
const { accountSelectorActive, menuItemStyles } = this.state const { accountSelectorActive, menuItemStyles } = this.state
@ -389,7 +421,8 @@ AccountDropdowns.defaultProps = {
AccountDropdowns.propTypes = { AccountDropdowns.propTypes = {
identities: PropTypes.objectOf(PropTypes.object), identities: PropTypes.objectOf(PropTypes.object),
selected: PropTypes.string, // TODO: refactor to be more explicit: selectedAddress selected: PropTypes.string,
keyrings: PropTypes.array,
} }
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {

View File

@ -23,7 +23,7 @@ Network.prototype.render = function () {
let iconName, hoverText let iconName, hoverText
if (networkNumber === 'loading') { if (networkNumber === 'loading') {
return h('span', { return h('span.pointer', {
style: { style: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -38,7 +38,7 @@ Network.prototype.render = function () {
}, },
src: 'images/loading.svg', src: 'images/loading.svg',
}), }),
h('i.fa.fa-sort-desc'), h('i.fa.fa-caret-down'),
]) ])
} else if (providerName === 'mainnet') { } else if (providerName === 'mainnet') {
hoverText = 'Main Ethereum Network' hoverText = 'Main Ethereum Network'
@ -77,7 +77,8 @@ Network.prototype.render = function () {
style: { style: {
color: '#039396', color: '#039396',
}}, }},
'Ethereum Main Net'), 'Main Network'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
case 'ropsten-test-network': case 'ropsten-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -90,6 +91,7 @@ Network.prototype.render = function () {
color: '#ff6666', color: '#ff6666',
}}, }},
'Ropsten Test Net'), 'Ropsten Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
case 'kovan-test-network': case 'kovan-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -102,6 +104,7 @@ Network.prototype.render = function () {
color: '#690496', color: '#690496',
}}, }},
'Kovan Test Net'), 'Kovan Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
case 'rinkeby-test-network': case 'rinkeby-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -114,6 +117,7 @@ Network.prototype.render = function () {
color: '#e7a218', color: '#e7a218',
}}, }},
'Rinkeby Test Net'), 'Rinkeby Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
default: default:
return h('.network-indicator', [ return h('.network-indicator', [
@ -129,6 +133,7 @@ Network.prototype.render = function () {
color: '#AEAEAE', color: '#AEAEAE',
}}, }},
'Private Network'), 'Private Network'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
} }
})(), })(),

View File

@ -38,7 +38,7 @@ PendingMsgDetails.prototype.render = function () {
// message data // message data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [ h('.flex-column.flex-space-between', [
h('label.font-small', 'MESSAGE'), h('label.font-small', 'MESSAGE'),
h('span.font-small', msgParams.data), h('span.font-small', msgParams.data),
]), ]),

View File

@ -18,6 +18,9 @@ PendingMsg.prototype.render = function () {
h('div', { h('div', {
key: msgData.id, key: msgData.id,
style: {
maxWidth: '350px',
},
}, [ }, [
// header // header
@ -32,10 +35,21 @@ PendingMsg.prototype.render = function () {
style: { style: {
margin: '10px', margin: '10px',
}, },
}, `Signing this message can have }, [
`Signing this message can have
dangerous side effects. Only sign messages from dangerous side effects. Only sign messages from
sites you fully trust with your entire account. sites you fully trust with your entire account.
This will be fixed in a future version.`), This dangerous method will be removed in a future version. `,
h('a', {
href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
style: { color: 'rgb(247, 134, 28)' },
onClick: (event) => {
event.preventDefault()
const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
global.platform.openWindow({ url })
},
}, 'Read more here.'),
]),
// message details // message details
h(PendingTxDetails, state), h(PendingTxDetails, state),

View File

@ -240,6 +240,15 @@ PendingTx.prototype.render = function () {
totalInETH, totalInETH,
} = this.getData() } = this.getData()
// This is from the latest master
// It handles some of the errors that we are not currently handling
// Leaving as comments fo reference
// const balanceBn = hexToBn(balance)
// const insufficientBalance = balanceBn.lt(maxCost)
// const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
// const showRejectAll = props.unconfTxListLength > 1
this.inputs = [] this.inputs = []
return ( return (
@ -335,6 +344,85 @@ PendingTx.prototype.render = function () {
]), ]),
]), ]),
// These are latest errors handling from master
// Leaving as comments as reference when we start implementing error handling
// h('style', `
// .conf-buttons button {
// margin-left: 10px;
// text-transform: uppercase;
// }
// `),
// txMeta.simulationFails ?
// h('.error', {
// style: {
// marginLeft: 50,
// fontSize: '0.9em',
// },
// }, 'Transaction Error. Exception thrown in contract code.')
// : null,
// !isValidAddress ?
// h('.error', {
// style: {
// marginLeft: 50,
// fontSize: '0.9em',
// },
// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
// : null,
// insufficientBalance ?
// h('span.error', {
// style: {
// marginLeft: 50,
// fontSize: '0.9em',
// },
// }, 'Insufficient balance for transaction')
// : null,
// // send + cancel
// h('.flex-row.flex-space-around.conf-buttons', {
// style: {
// display: 'flex',
// justifyContent: 'flex-end',
// margin: '14px 25px',
// },
// }, [
// h('button', {
// onClick: (event) => {
// this.resetGasFields()
// event.preventDefault()
// },
// }, 'Reset'),
// // Accept Button or Buy Button
// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
// h('input.confirm.btn-green', {
// type: 'submit',
// value: 'SUBMIT',
// style: { marginLeft: '10px' },
// disabled: buyDisabled,
// }),
// h('button.cancel.btn-red', {
// onClick: props.cancelTransaction,
// }, 'Reject'),
// ]),
// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
// style: {
// display: 'flex',
// justifyContent: 'flex-end',
// margin: '14px 25px',
// },
// }, [
// h('button.cancel.btn-red', {
// onClick: props.cancelAllTransactions,
// }, 'Reject All'),
// ]) : null,
// ]),
// ])
// )
// }
]), ]),
h('form#pending-tx-form.flex-column.flex-center', { h('form#pending-tx-form.flex-column.flex-center', {

View File

@ -48,10 +48,28 @@ TokenList.prototype.render = function () {
if (error) { if (error) {
log.error(error) log.error(error)
return this.message('There was a problem loading your token balances.') return h('.hotFix', {
style: {
padding: '80px',
},
}, [
'We had trouble loading your token balances. You can view them ',
h('span.hotFix', {
style: {
color: 'rgba(247, 134, 28, 1)',
cursor: 'pointer',
},
onClick: () => {
global.platform.openWindow({
url: `https://ethplorer.io/address/${userAddress}`,
})
},
}, 'here'),
])
} }
return h('div', tokens.map((tokenData) => h(TokenCell, tokenData))) return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
} }
TokenList.prototype.message = function (body) { TokenList.prototype.message = function (body) {
@ -84,7 +102,7 @@ TokenList.prototype.createFreshTokenTracker = function () {
this.tracker = new TokenTracker({ this.tracker = new TokenTracker({
userAddress, userAddress,
provider: global.ethereumProvider, provider: global.ethereumProvider,
tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), tokens: this.props.tokens,
pollingInterval: 8000, pollingInterval: 8000,
}) })
@ -149,4 +167,3 @@ function uniqueMergeTokens (tokensA, tokensB = []) {
}) })
return result return result
} }

View File

@ -60,17 +60,8 @@ TransactionListItem.prototype.render = function () {
}, [ }, [
h('.identicon-wrapper.flex-column.flex-center.select-none', [ h('.identicon-wrapper.flex-column.flex-center.select-none', [
h('.pop-hover', {
onClick: (event) => {
event.stopPropagation()
if (!isTx || isPending) return
var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}`
global.platform.openWindow({ url })
},
}, [
h(TransactionIcon, { txParams, transaction, isTx, isMsg }), h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
]), ]),
]),
h(Tooltip, { h(Tooltip, {
title: 'Transaction Number', title: 'Transaction Number',

View File

@ -76,6 +76,7 @@ ConfirmTxScreen.prototype.render = function () {
cancelMessage: this.cancelMessage.bind(this, txData), cancelMessage: this.cancelMessage.bind(this, txData),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
}) })
} }
function currentTxView (opts) { function currentTxView (opts) {
@ -116,6 +117,12 @@ ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) {
this.props.dispatch(actions.cancelTx(txData)) this.props.dispatch(actions.cancelTx(txData))
} }
ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) {
this.stopPropagation(event)
event.preventDefault()
this.props.dispatch(actions.cancelAllTx(unconfTxList))
}
ConfirmTxScreen.prototype.signMessage = function (msgData, event) { ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
log.info('conf-tx.js: signing message') log.info('conf-tx.js: signing message')
var params = msgData.msgParams var params = msgData.msgParams

View File

@ -5,7 +5,8 @@ const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const currencies = require('./conversion.json').rows const currencies = require('./conversion.json').rows
const validUrl = require('valid-url') const validUrl = require('valid-url')
const copyToClipboard = require('copy-to-clipboard') const exportAsFile = require('./util').exportAsFile
module.exports = connect(mapStateToProps)(ConfigScreen) module.exports = connect(mapStateToProps)(ConfigScreen)
@ -110,9 +111,9 @@ ConfigScreen.prototype.render = function () {
alignSelf: 'center', alignSelf: 'center',
}, },
onClick (event) { onClick (event) {
copyToClipboard(window.logState()) exportAsFile('MetaMask State Logs', window.logState())
}, },
}, 'Copy State Logs'), }, 'Download State Logs'),
]), ]),
h('hr.horizontal-line'), h('hr.horizontal-line'),

View File

@ -238,7 +238,7 @@ hr.horizontal-line {
border-radius: 10px; border-radius: 10px;
height: 20px; height: 20px;
min-width: 20px; min-width: 20px;
position: relative; position: absolute;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -103,7 +103,7 @@ InfoScreen.prototype.render = function () {
[ [
h('div.fa.fa-support', [ h('div.fa.fa-support', [
h('a.info', { h('a.info', {
href: 'http://metamask.consensyssupport.happyfox.com', href: 'https://support.metamask.io',
target: '_blank', target: '_blank',
}, 'Visit our Support Center'), }, 'Visit our Support Center'),
]), ]),

View File

@ -3,6 +3,7 @@ const Component = require('react').Component
const connect = require('react-redux').connect const connect = require('react-redux').connect
const h = require('react-hyperscript') const h = require('react-hyperscript')
const actions = require('../../actions') const actions = require('../../actions')
const exportAsFile = require('../../util').exportAsFile
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
@ -65,8 +66,17 @@ CreateVaultCompleteScreen.prototype.render = function () {
style: { style: {
margin: '24px', margin: '24px',
fontSize: '0.9em', fontSize: '0.9em',
marginBottom: '10px',
}, },
}, 'I\'ve copied it somewhere safe'), }, 'I\'ve copied it somewhere safe'),
h('button.primary', {
onClick: () => exportAsFile(`MetaMask Seed Words`, seed),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
]) ])
) )
} }

View File

@ -42,7 +42,10 @@ function rootReducer (state, action) {
} }
window.logState = function () { window.logState = function () {
var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) let state = window.METAMASK_CACHED_LOG_STATE
const version = global.platform.getVersion()
state.version = version
let stateString = JSON.stringify(state, removeSeedWords, 2)
return stateString return stateString
} }

View File

@ -80,7 +80,7 @@ UnlockScreen.prototype.render = function () {
color: 'rgb(247, 134, 28)', color: 'rgb(247, 134, 28)',
textDecoration: 'underline', textDecoration: 'underline',
}, },
}, 'I forgot my password.'), }, 'Restore from seed phrase'),
]), ]),
]) ])
) )

View File

@ -53,6 +53,7 @@ module.exports = {
getTxFeeBn, getTxFeeBn,
shortenBalance, shortenBalance,
getContractAtAddress, getContractAtAddress,
exportAsFile: exportAsFile,
} }
function valuesFor (obj) { function valuesFor (obj) {
@ -250,3 +251,18 @@ function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimi
function getContractAtAddress (tokenAddress) { function getContractAtAddress (tokenAddress) {
return global.eth.contract(abi).at(tokenAddress) return global.eth.contract(abi).at(tokenAddress)
} }
function exportAsFile (filename, data) {
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
const blob = new Blob([data], {type: 'text/csv'})
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename)
} else {
const elem = window.document.createElement('a')
elem.href = window.URL.createObjectURL(blob)
elem.download = filename
document.body.appendChild(elem)
elem.click()
document.body.removeChild(elem)
}
}

View File

@ -3,19 +3,19 @@ module.exports = function (address, network) {
let link let link
switch (net) { switch (net) {
case 1: // main net case 1: // main net
link = `http://etherscan.io/address/${address}` link = `https://etherscan.io/address/${address}`
break break
case 2: // morden test net case 2: // morden test net
link = `http://morden.etherscan.io/address/${address}` link = `https://morden.etherscan.io/address/${address}`
break break
case 3: // ropsten test net case 3: // ropsten test net
link = `http://ropsten.etherscan.io/address/${address}` link = `https://ropsten.etherscan.io/address/${address}`
break break
case 4: // rinkeby test net case 4: // rinkeby test net
link = `http://rinkeby.etherscan.io/address/${address}` link = `https://rinkeby.etherscan.io/address/${address}`
break break
case 42: // kovan test net case 42: // kovan test net
link = `http://kovan.etherscan.io/address/${address}` link = `https://kovan.etherscan.io/address/${address}`
break break
default: default:
link = '' link = ''