Merge pull request #2116 from chikeichan/nm

[NewUI] Fix merge conflict with latest master
feature/default_network_editable
Chi Kei Chan 7 years ago committed by GitHub
commit a190bb6043
  1. 55
      CHANGELOG.md
  2. 5
      app/manifest.json
  3. 8
      app/scripts/background.js
  4. 45
      app/scripts/contentscript.js
  5. 46
      app/scripts/controllers/transactions.js
  6. 3
      app/scripts/keyring-controller.js
  7. 58
      app/scripts/lib/auto-reload.js
  8. 15
      app/scripts/lib/createLoggerMiddleware.js
  9. 9
      app/scripts/lib/createOriginMiddleware.js
  10. 13
      app/scripts/lib/createProviderMiddleware.js
  11. 82
      app/scripts/lib/inpage-provider.js
  12. 107
      app/scripts/lib/nonce-tracker.js
  13. 48
      app/scripts/lib/obj-multiplex.js
  14. 6
      app/scripts/lib/pending-tx-tracker.js
  15. 16
      app/scripts/lib/port-stream.js
  16. 22
      app/scripts/lib/stream-utils.js
  17. 37
      app/scripts/lib/tx-state-history-helper.js
  18. 91
      app/scripts/metamask-controller.js
  19. 52
      app/scripts/migrations/018.js
  20. 83
      app/scripts/migrations/019.js
  21. 2
      app/scripts/migrations/index.js
  22. 15
      circle.yml
  23. 4
      development/uiStore.js
  24. 0
      docs/add-to-firefox.md
  25. 61
      karma.conf.js
  26. 4
      mascara/src/lib/setup-iframe.js
  27. 4
      mascara/src/proxy.js
  28. 13
      mock-dev.js
  29. 23
      package.json
  30. 3053
      test/data/v17-long-history.json
  31. 7
      test/integration/helpers.js
  32. 19
      test/integration/index.js
  33. 146
      test/integration/lib/first-time.js
  34. 4
      test/lib/mock-store.js
  35. 40
      test/lib/mock-tx-gen.js
  36. 85
      test/unit/actions/tx_test.js
  37. 208
      test/unit/nonce-tracker-test.js
  38. 35
      test/unit/tx-controller-test.js
  39. 26
      test/unit/tx-state-history-helper-test.js
  40. 23
      test/unit/tx-state-history-helper.js
  41. 10
      testem.yml
  42. 13
      ui/app/actions.js
  43. 29
      ui/app/add-token.js
  44. 2
      ui/app/app.js
  45. 28
      ui/app/components/account-export.js
  46. 45
      ui/app/components/dropdowns/components/account-dropdowns.js
  47. 11
      ui/app/components/network.js
  48. 2
      ui/app/components/pending-msg-details.js
  49. 18
      ui/app/components/pending-msg.js
  50. 88
      ui/app/components/pending-tx.js
  51. 23
      ui/app/components/token-list.js
  52. 9
      ui/app/components/transaction-list-item.js
  53. 7
      ui/app/conf-tx.js
  54. 7
      ui/app/config.js
  55. 2
      ui/app/css/itcss/tools/utilities.scss
  56. 2
      ui/app/info.js
  57. 10
      ui/app/keychains/hd/create-vault-complete.js
  58. 5
      ui/app/reducers.js
  59. 2
      ui/app/unlock.js
  60. 16
      ui/app/util.js
  61. 10
      ui/lib/account-link.js
  62. 599
      yarn.lock

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

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

@ -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.')
} }
// //

@ -1,11 +1,12 @@
const fs = require('fs')
const path = require('path')
const pump = require('pump')
const LocalMessageDuplexStream = require('post-message-stream') const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong') const PongStream = require('ping-pong-stream/pong')
const PortStream = require('./lib/port-stream.js') const ObjectMultiplex = require('obj-multiplex')
const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('extensionizer') const extension = require('extensionizer')
const PortStream = require('./lib/port-stream.js')
const fs = require('fs')
const path = require('path')
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 () {

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

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

@ -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
const currentNetwork = state.networkVersion
// 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
if (curentNetVersion !== networkVersion && web3._used) { // initiate page reload
const timeSinceUse = Date.now() - web3._used 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)
} }
}) })
} }

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

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

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

@ -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
// handle sendAsync requests via asyncProvider const rpcEngine = new RpcEngine()
self.sendAsync = function (payload, cb) { rpcEngine.push(createIdRemapMiddleware())
// rewrite request ids rpcEngine.push(streamMiddleware)
var request = eachJsonMessage(payload, (message) => { self.rpcEngine = rpcEngine
var newId = createRandomId()
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)
})
} }
// handle sendAsync requests via asyncProvider
// also remap ids inbound and outbound
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
const self = this
self.rpcEngine.handle(payload, cb)
} }
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

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

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

@ -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.')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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}`)
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)
// }
// })
wait().then(function() { async function runFirstTimeUsageTest(assert, done) {
app = $('iframe').contents().find('#app-content .mock-app-root')
const recurseNotices = function () { await timeout()
let button = app.find('button')
const app = $('#app-content .mock-app-root')
// recurse notices
while (true) {
const button = app.find('button')
if (button.html() === 'Accept') { if (button.html() === 'Accept') {
let termsPage = app.find('.markdown')[0] // still notices to accept
const termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight termsPage.scrollTop = termsPage.scrollHeight
return wait().then(() => { await timeout()
button.click() button.click()
return wait() await timeout()
}).then(() => {
return recurseNotices()
})
} else { } else {
return wait() // exit loop
break
} }
} }
return recurseNotices()
}).then(function() { await timeout()
// Scroll through terms // Scroll through terms
var title = app.find('h1').text() const title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen') assert.equal(title, 'MetaMask', 'title screen')
// enter password // enter password
var pwBox = app.find('#password-box')[0] const pwBox = app.find('#password-box')[0]
var confBox = app.find('#password-box-confirm')[0] const confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD pwBox.value = PASSWORD
confBox.value = PASSWORD confBox.value = PASSWORD
return wait() await timeout()
}).then(function() {
// create vault // create vault
var createButton = app.find('button.primary')[0] const createButton = app.find('button.primary')[0]
createButton.click() createButton.click()
return wait(1500) await timeout(1500)
}).then(function() {
var created = app.find('h3')[0] const created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen') assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button // Agree button
var button = app.find('button')[0] const button = app.find('button')[0]
assert.ok(button, 'button present') assert.ok(button, 'button present')
button.click() button.click()
return wait(1000) await timeout(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0] const detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.') assert.ok(detail, 'Account detail section loaded.')
var sandwich = app.find('.sandwich-expando')[0] const sandwich = app.find('.sandwich-expando')[0]
sandwich.click() sandwich.click()
return wait() await timeout()
}).then(function() {
var sandwich = app.find('.menu-droppo')[0] const menu = app.find('.menu-droppo')[0]
var children = sandwich.children const children = menu.children
var lock = children[children.length - 2] const lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found') assert.ok(lock, 'Lock menu item found')
lock.click() lock.click()
return wait(1000) await timeout(1000)
}).then(function() {
var pwBox = app.find('#password-box')[0] const pwBox2 = app.find('#password-box')[0]
pwBox.value = PASSWORD pwBox2.value = PASSWORD
var createButton = app.find('button.primary')[0] const createButton2 = app.find('button.primary')[0]
createButton.click() createButton2.click()
return wait(1000) await timeout(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0] const detail2 = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded again.') assert.ok(detail2, 'Account detail section loaded again.')
return wait() await timeout()
}).then(function (){
var qrButton = app.find('.fa.fa-ellipsis-h')[0] // open account settings dropdown // open account settings dropdown
const qrButton = app.find('.fa.fa-ellipsis-h')[0]
qrButton.click() qrButton.click()
return wait(1000) await timeout(1000)
}).then(function (){
var qrButton = app.find('.dropdown-menu-item')[1] // qr code item // qr code item
qrButton.click() const qrButton2 = app.find('.dropdown-menu-item')[1]
qrButton2.click()
return wait(1000) await timeout(1000)
}).then(function (){
var qrHeader = app.find('.qr-header')[0] const qrHeader = app.find('.qr-header')[0]
var qrContainer = app.find('#qr-container')[0] const qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.') assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found') assert.ok(qrContainer, 'QR Container found')
return wait() await timeout()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0] const networkMenu = app.find('.network-indicator')[0]
networkMenu.click() networkMenu.click()
return wait() await timeout()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0] const networkMenu2 = app.find('.network-indicator')[0]
var children = networkMenu.children const children2 = networkMenu2.children
children.length[3] children2.length[3]
assert.ok(children, 'All network options present') assert.ok(children2, 'All network options present')
}
done() function timeout(time) {
}) return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
}) })
}

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

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

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

@ -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
describe('#getNonceLock', function () {
describe('with 3 confirmed and 1 pending', function () {
beforeEach(function () { beforeEach(function () {
pendingTxs = [{ const txGen = new MockTxGen()
'status': 'submitted', confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
'txParams': { pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926', nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
'gas': '0x30d40', })
'value': '0x0',
'nonce': '0x0',
},
}]
it('should return 4', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
getPendingTransactions = () => pendingTxs it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
provider = { this.timeout(15000)
sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) }, const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
_blockTracker: { assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
getCurrentBlock: () => '0x11b568', await nonceLock.releaseLock()
},
}
nonceTracker = new NonceTracker({
provider,
getPendingTransactions,
}) })
}) })
describe('#getNonceLock', function () { describe('with no previous txs', function () {
it('should work', async 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) 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, '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,
})
}

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

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

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

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

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

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

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

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

@ -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) => {
@ -420,5 +453,11 @@ const mapDispatchToProps = (dispatch) => {
} }
} }
module.exports = connect(null, mapDispatchToProps)(AccountDropdowns) function mapStateToProps (state) {
return {
keyrings: state.metamask.keyrings,
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns)

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

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

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

@ -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', {

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save