Merge branch 'master' into AddBalanceController

feature/default_network_editable
Dan Finlay 7 years ago
commit f9d2f523c6
  1. 14
      CHANGELOG.md
  2. 1
      README.md
  3. 2
      app/manifest.json
  4. 8
      app/scripts/background.js
  5. 45
      app/scripts/contentscript.js
  6. 15
      app/scripts/lib/createLoggerMiddleware.js
  7. 9
      app/scripts/lib/createOriginMiddleware.js
  8. 13
      app/scripts/lib/createProviderMiddleware.js
  9. 69
      app/scripts/lib/inpage-provider.js
  10. 48
      app/scripts/lib/obj-multiplex.js
  11. 12
      app/scripts/lib/pending-tx-tracker.js
  12. 16
      app/scripts/lib/port-stream.js
  13. 24
      app/scripts/lib/stream-utils.js
  14. 89
      app/scripts/metamask-controller.js
  15. 2
      circle.yml
  16. 4
      development/index.html
  17. 5
      development/test.html
  18. 65
      docs/porting_to_new_environment.md
  19. 3
      mascara/src/proxy.js
  20. 49
      mascara/src/ui.js
  21. 21
      mascara/test/index.html
  22. 119
      mascara/test/lib/first-time.js
  23. 12
      mascara/test/test-ui.js
  24. 13
      mascara/test/testem.yml
  25. 5
      mascara/test/window-load.js
  26. 5
      mock-dev.js
  27. 52
      package.json
  28. 6
      test/base.conf.js
  29. 8
      test/flat.conf.js
  30. 27
      test/integration/lib/first-time.js
  31. 17
      test/mascara.conf.js
  32. 4
      ui-dev.js
  33. 65
      ui/app/components/pending-tx.js
  34. 2
      ui/app/components/tooltip.js
  35. 2
      ui/app/components/transaction-list-item-icon.js
  36. 2
      ui/app/components/transaction-list-item.js
  37. 2
      ui/app/info.js
  38. 5
      ui/app/send.js
  39. 7
      ui/app/util.js

@ -2,9 +2,23 @@
## Current Master ## Current Master
- Fix bug where metamask-dapp connections are lost on rpc error
- Fix bug that would sometimes display transactions as failed that could be successfully mined.
## 3.10.2 2017-9-18
rollback to 3.10.0 due to bug
## 3.10.1 2017-9-18
- Add ability to export private keys as a file. - Add ability to export private keys as a file.
- Add ability to export seed words as a file. - Add ability to export seed words as a file.
- Changed state logs to a file download than a clipboard copy. - Changed state logs to a file download than a clipboard copy.
- Add specific error for failed recipient address checksum.
- Fixed a long standing memory leak associated with filters installed by dapps
- Fix link to support center.
- Fixed tooltip icon locations to avoid overflow.
- Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher)
## 3.10.0 2017-9-11 ## 3.10.0 2017-9-11

@ -71,6 +71,7 @@ To write tests that will be run in the browser using QUnit, add your test files
- [How to live reload on local dependency changes](./docs/developing-on-deps.md) - [How to live reload on local dependency changes](./docs/developing-on-deps.md)
- [How to add new networks to the Provider Menu](./docs/adding-new-networks.md) - [How to add new networks to the Provider Menu](./docs/adding-new-networks.md)
- [How to manage notices that appear when the app starts up](./docs/notices.md) - [How to manage notices that appear when the app starts up](./docs/notices.md)
- [How to port MetaMask to a new platform](./docs/porting_to_new_environment.md)
- [How to generate a visualization of this repository's development](./docs/development-visualization.md) - [How to generate a visualization of this repository's development](./docs/development-visualization.md)
[1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A [1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.10.0", "version": "3.10.2",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",

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

@ -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,64 +11,46 @@ function MetamaskInpageProvider (connectionStream) {
const self = this const self = this
// setup connectionStream multiplexing // setup connectionStream multiplexing
var multiStream = self.multiStream = ObjectMultiplex() const mux = self.mux = new ObjectMultiplex()
pipe( pump(
connectionStream, connectionStream,
multiStream, mux,
connectionStream, connectionStream,
(err) => logStreamDisconnectWarning('MetaMask', err) (err) => logStreamDisconnectWarning('MetaMask', err)
) )
// subscribe to metamask public config (one-way) // subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
pipe( pump(
multiStream.createStream('publicConfig'), mux.createStream('publicConfig'),
self.publicConfigStore, self.publicConfigStore,
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
) )
// ignore phishing warning message (handled elsewhere) // ignore phishing warning message (handled elsewhere)
multiStream.ignoreStream('phishing') mux.ignoreStream('phishing')
// connect to async provider // connect to async provider
const asyncProvider = self.asyncProvider = new StreamProvider() const streamMiddleware = createStreamMiddleware()
pipe( pump(
asyncProvider, streamMiddleware.stream,
multiStream.createStream('provider'), mux.createStream('provider'),
asyncProvider, streamMiddleware.stream,
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
) )
// start and stop polling to unblock first block lock
self.idMap = {} // handle sendAsync requests via dapp-side rpc engine
const rpcEngine = new RpcEngine()
rpcEngine.push(createIdRemapMiddleware())
rpcEngine.push(streamMiddleware)
self.rpcEngine = rpcEngine
} }
// handle sendAsync requests via asyncProvider // handle sendAsync requests via asyncProvider
// also remap ids inbound and outbound // also remap ids inbound and outbound
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
const self = this const self = this
self.rpcEngine.handle(payload, cb)
// rewrite request ids
const request = eachJsonMessage(payload, (_message) => {
const message = Object.assign({}, _message)
const newId = createRandomId()
self.idMap[newId] = message.id
message.id = newId
return message
})
// forward to asyncProvider
self.asyncProvider.sendAsync(request, (err, _res) => {
if (err) return cb(err)
// transform messages to original ids
const res = eachJsonMessage(_res, (message) => {
const oldId = self.idMap[message.id]
delete self.idMap[message.id]
message.id = oldId
return message
})
cb(null, res)
})
} }
@ -124,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,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
}

@ -76,6 +76,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
Dont marked as failed if the error is a "known" transaction warning Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce "there is already a transaction with the same sender-nonce
but higher/same gas price" but higher/same gas price"
Also don't mark as failed if it has ever been broadcast successfully.
A successful broadcast means it may still be mined.
*/ */
const errorMessage = err.message.toLowerCase() const errorMessage = err.message.toLowerCase()
const isKnownTx = ( const isKnownTx = (
@ -88,6 +91,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
// other // other
|| errorMessage.includes('gateway timeout') || errorMessage.includes('gateway timeout')
|| errorMessage.includes('nonce too low') || errorMessage.includes('nonce too low')
|| txMeta.retryCount > 1
) )
// ignore resubmit warnings, return early // ignore resubmit warnings, return early
if (isKnownTx) return if (isKnownTx) return
@ -117,10 +121,12 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
// Only auto-submit already-signed txs: // Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return if (!('rawTx' in txMeta)) return
// Increment a try counter.
txMeta.retryCount++
const rawTx = txMeta.rawTx const rawTx = txMeta.rawTx
return await this.publishTransaction(rawTx) const txHash = await this.publishTransaction(rawTx)
// Increment successful tries:
txMeta.retryCount++
return txHash
} }
async _checkPendingTx (txMeta) { async _checkPendingTx (txMeta) {

@ -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,
if (err) console.error(err) mux,
}) connectionStream,
endOfStream(connectionStream, function (err) { (err) => {
if (err) console.error(err) if (err) console.error(err)
mx.destroy() }
}) )
return mx return mux
} }

@ -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 AccountTracker = require('./lib/account-tracker') const AccountTracker = require('./lib/account-tracker')
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')
@ -25,8 +31,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 {
@ -78,6 +82,7 @@ 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)
@ -116,7 +121,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,
accountTracker: this.accountTracker, accountTracker: this.accountTracker,
}) })
@ -357,36 +362,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)
@ -394,27 +406,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 }))
engine.push(filterMiddleware)
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)
} }
if (request.isMetamaskInternal) return )
log.info(`RPC (${originDomain}):`, request, '->', response)
}
} }
setupPublicConfig (outStream) { setupPublicConfig (outStream) {
pipe( pump(
this.publicConfigStore, this.publicConfigStore,
outStream outStream,
(err) => {
if (err) log.error(err)
}
) )
} }

@ -3,7 +3,7 @@ machine:
version: 8.1.4 version: 8.1.4
test: test:
override: override:
- "npm run ci" - "npm test"
dependencies: dependencies:
pre: pre:
- sudo apt-get update - sudo apt-get update

@ -14,13 +14,13 @@
</body> </body>
<style> <style>
html, body, #app-content, .super-dev-container { html, body, #test-container, .super-dev-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
background: white; background: white;
} }
.mock-app-root { #app-content {
background: #F7F7F7; background: #F7F7F7;
} }
</style> </style>

@ -18,13 +18,14 @@
</body> </body>
<style> <style>
html, body, #app-content, .super-dev-container { html, body, #test-container, .super-dev-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
background: white; background: white;
} }
.mock-app-root {
#app-content {
background: #F7F7F7; background: #F7F7F7;
} }
</style> </style>

@ -0,0 +1,65 @@
# Guide to Porting MetaMask to a New Environment
MetaMask has been under continuous development for nearly two years now, and we’ve gradually discovered some very useful abstractions, that have allowed us to grow more easily. A couple of those layers together allow MetaMask to be ported to new environments and contexts increasingly easily.
### The MetaMask Controller
The core functionality of MetaMask all lives in what we call [The MetaMask Controller](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js). Our goal for this file is for it to eventually be its own javascript module that can be imported into any JS-compatible context, allowing it to fully manage an app's relationship to Ethereum.
The MM Controller exposes most of its functionality via two methods:
- [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) - This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated)
- [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335) - Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works!
### The UI
The MetaMask UI is essentially just a website that can be configured by passing it the API and state subscriptions from above. Anyone could make a UI that consumes these, effectively reskinning MetaMask.
You can see this in action in our file [ui/index.js](https://github.com/MetaMask/metamask-extension/blob/master/ui/index.js). There you can see an argument being passed in named `accountManager`, which is essentially a MetaMask controller (forgive its really outdated parameter name!). With access to that object, the UI is able to initialize a whole React/Redux app that relies on this API for its account/blockchain-related/persistent states.
## Putting it Together
As an example, a WebExtension is always defined by a `manifest.json` file. [In ours](https://github.com/MetaMask/metamask-extension/blob/master/app/manifest.json#L31), you can see that [background.js](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/background.js) is defined as a script to run in the background, and this is the file that we use to initialize the MetaMask controller.
In that file, there's a lot going on, so it's maybe worth focusing on our MetaMask controller constructor to start. It looks something like this:
```javascript
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
// initial state
initState,
// platform specific api
platform,
})
```
Since `background.js` is essentially the Extension setup file, we can see it doing all the things specific to the extension platform:
- Defining how to open the UI for new messages, transactions, and even requests to unlock (reveal to the site) their account.
- Provide the instance's initial state, leaving MetaMask persistence to the platform.
- Providing a `platform` object. This is becoming our catch-all adapter for platforms to define a few other platform-variant features we require, like opening a web link. (Soon we will be moving encryption out here too, since our browser-encryption isn't portable enough!)
## Ports, streams, and Web3!
Everything so far has been enough to create a MetaMask wallet on virtually any platform that runs JS, but MetaMask's most unique feature isn't being a wallet, it's providing an Ethereum-enabled JavaScript context to websites.
MetaMask has two kinds of [duplex stream APIs](https://github.com/substack/stream-handbook#duplex) that it exposes:
- [metamask.setupTrustedCommunication(connectionStream, originDomain)](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L352) - This stream is used to connect the user interface over a remote port, and may not be necessary for contexts where the interface and the metamask-controller share a process.
- [metamask.setupUntrustedCommunication(connectionStream, originDomain)](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L337) - This method is used to connect a new web site's web3 API to MetaMask's blockchain connection. Additionally, the `originDomain` is used to block detected phishing sites.
### Web3 as a Stream
If you are making a MetaMask-powered browser for a new platform, one of the trickiest tasks will be injecting the Web3 API into websites that are visited. On WebExtensions, we actually have to pipe data through a total of three JS contexts just to let sites talk to our background process (site -> contentscript -> background).
To make this as easy as possible, we use one of our favorite internal tools, [web3-provider-engine](https://www.npmjs.com/package/web3-provider-engine) to construct a custom web3 provider object whose source of truth is a stream that we connect to remotely.
To see how we do that, you can refer to the [inpage script](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/inpage.js) that we inject into every website. There you can see it creates a multiplex stream to the background, and uses it to initialize what we call the [inpage-provider](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/lib/inpage-provider.js), which you can see stubs a few methods out, but mostly just passes calls to `sendAsync` through the stream it's passed! That's really all the magic that's needed to create a web3-like API in a remote context, once you have a stream to MetaMask available.
In `inpage.js` you can see we create a `PortStream`, that's just a class we use to wrap WebExtension ports as streams, so we can reuse our favorite stream abstraction over the more irregular API surface of the WebExtension. In a new platform, you will probably need to construct this stream differently. The key is that you need to construct a stream that talks from the site context to the background. Once you have that set up, it works like magic!
If streams seem new and confusing to you, that's ok, they can seem strange at first. To help learn them, we highly recommend reading Substack's [Stream Handbook](https://github.com/substack/stream-handbook), or going through NodeSchool's interactive command-line class [Stream Adventure](https://github.com/workshopper/stream-adventure), also maintained by Substack.
## Conclusion
I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)!

@ -1,7 +1,6 @@
const createParentStream = 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')
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000 let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({ const background = new SWcontroller({
@ -12,7 +11,7 @@ const background = new SWcontroller({
}) })
const pageStream = createParentStream() const pageStream = createParentStream()
background.on('ready', (_) => { background.on('ready', () => {
let swStream = SwStream({ let swStream = SwStream({
serviceWorker: background.controller, serviceWorker: background.controller,
context: 'dapp', context: 'dapp',

@ -2,8 +2,6 @@ const injectCss = require('inject-css')
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 MetaMaskUiCss = require('../../ui/css') const MetaMaskUiCss = require('../../ui/css')
const setupIframe = require('./lib/setup-iframe.js')
const MetamaskInpageProvider = require('../../app/scripts/lib/inpage-provider.js')
const MetamascaraPlatform = require('../../app/scripts/platforms/window') const MetamascaraPlatform = require('../../app/scripts/platforms/window')
const startPopup = require('../../app/scripts/popup-core') const startPopup = require('../../app/scripts/popup-core')
@ -17,6 +15,7 @@ const container = document.getElementById('app-content')
var name = 'popup' var name = 'popup'
window.METAMASK_UI_TYPE = name window.METAMASK_UI_TYPE = name
window.METAMASK_PLATFORM_TYPE = 'mascara'
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000 let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
@ -32,25 +31,39 @@ const connectApp = function (readSw) {
serviceWorker: background.controller, serviceWorker: background.controller,
context: name, context: name,
}) })
startPopup({container, connectionStream}, (err, store) => { return new Promise((resolve, reject) => {
if (err) return displayCriticalError(err) startPopup({ container, connectionStream }, (err, store) => {
store.subscribe(() => { console.log('hello from MetaMascara ui!')
const state = store.getState() if (err) reject(err)
if (state.appState.shouldClose) window.close() store.subscribe(() => {
const state = store.getState()
if (state.appState.shouldClose) window.close()
})
resolve()
}) })
}) })
} }
background.on('ready', (sw) => { background.on('ready', async (sw) => {
background.removeListener('updatefound', connectApp) try {
connectApp(sw) background.removeListener('updatefound', connectApp)
await timeout(1000)
await connectApp(sw)
console.log('hello from cb ready event!')
} catch (e) {
console.error(e)
}
}) })
background.on('updatefound', () => window.location.reload()) background.on('updatefound', windowReload)
background.startWorker() background.startWorker()
.then(() => {
setTimeout(() => { function windowReload() {
const appContent = document.getElementById(`app-content`) if (window.METAMASK_SKIP_RELOAD) return
if (!appContent.children.length) window.location.reload() window.location.reload()
}, 2000) }
})
console.log('hello from MetaMascara ui!') function timeout (time) {
return new Promise((resolve) => {
setTimeout(resolve, time || 1500)
})
}

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>QUnit Example</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.0.0.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-2.0.0.js"></script>
<script src="./jquery-3.1.0.min.js"></script>
<script src="./helpers.js"></script>
<script src="./test-bundle.js"></script>
<script src="/testem.js"></script>
<div id="app-content"></div>
<script src="./bundle.js"></script>
</body>
</html>

@ -1,119 +0,0 @@
const PASSWORD = 'password123'
QUnit.module('first time usage')
QUnit.test('render init screen', function (assert) {
var done = assert.async()
let app
wait(1000).then(function() {
app = $('#app-content').contents()
const recurseNotices = function () {
let button = app.find('button')
if (button.html() === 'Accept') {
let termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
return wait().then(() => {
button.click()
return wait()
}).then(() => {
return recurseNotices()
})
} else {
return wait()
}
}
return recurseNotices()
}).then(function() {
// Scroll through terms
var title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
// enter password
var pwBox = app.find('#password-box')[0]
var confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
return wait()
}).then(function() {
// create vault
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1500)
}).then(function() {
var created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button
var button = app.find('button')[0]
assert.ok(button, 'button present')
button.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
var sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
return wait()
}).then(function() {
var sandwich = app.find('.menu-droppo')[0]
var children = sandwich.children
var lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
return wait(1000)
}).then(function() {
var pwBox = app.find('#password-box')[0]
pwBox.value = PASSWORD
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded again.')
return wait()
}).then(function (){
var qrButton = app.find('.fa.fa-qrcode')[0]
qrButton.click()
return wait(1000)
}).then(function (){
var qrHeader = app.find('.qr-header')[0]
var qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
networkMenu.click()
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
var children = networkMenu.children
children.length[3]
assert.ok(children, 'All network options present')
done()
})
})

@ -0,0 +1,12 @@
const Helper = require('./util/mascara-test-helper.js')
window.addEventListener('load', () => {
window.METAMASK_SKIP_RELOAD = true
// inject app container
const body = document.body
const container = document.createElement('div')
container.id = 'app-content'
body.appendChild(container)
// start ui
require('../src/ui.js')
})

@ -1,13 +0,0 @@
launch_in_dev:
- Chrome
- Firefox
- Opera
launch_in_ci:
- Chrome
- Firefox
- Opera
framework:
- qunit
before_tests: "npm run mascaraCi"
after_tests: "rm ./background.js ./test-bundle.js ./bundle.js"
test_page: "./index.html"

@ -1,5 +0,0 @@
const Helper = require('./util/mascara-test-helper.js')
window.addEventListener('load', () => {
require('../src/ui.js')
})

@ -94,9 +94,8 @@ startApp()
function startApp(){ function startApp(){
const body = document.body const body = document.body
const container = document.createElement('div') const container = document.createElement('div')
container.id = 'app-content' container.id = 'test-container'
body.appendChild(container) body.appendChild(container)
console.log('container', container)
render( render(
h('.super-dev-container', [ h('.super-dev-container', [
@ -113,7 +112,7 @@ function startApp(){
h(Selector, { actions, selectedKey: selectedView, states, store }), h(Selector, { actions, selectedKey: selectedView, states, store }),
h('.mock-app-root', { h('#app-content', {
style: { style: {
height: '500px', height: '500px',
width: '360px', width: '360px',

@ -6,30 +6,33 @@
"scripts": { "scripts": {
"start": "npm run dev", "start": "npm run dev",
"dev": "gulp dev --debug", "dev": "gulp dev --debug",
"disc": "gulp disc --debug", "ui": "npm run test:flat:build:states && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
"dist": "npm run clear && npm install && gulp dist",
"test": "npm run lint && npm run test-unit && npm run test-integration",
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"single-test": "METAMASK_ENV=test mocha --require test/helper.js",
"test-integration": "npm run buildMock && npm run buildCiUnits && karma start",
"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",
"lint": "gulp lint",
"buildCiUnits": "node test/integration/index.js",
"watch": "mocha watch --recursive \"test/unit/**/*.js\"",
"genStates": "node development/genStates.js",
"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", "watch": "mocha watch --recursive \"test/unit/**/*.js\"",
"mascara": "node ./mascara/example/server",
"dist": "npm run dist:clear && npm install && gulp dist",
"dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
"test": "npm run lint && npm run test:coverage && npm run test:integration",
"test:unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test:single": "METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:flat && npm run test:mascara",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
"test:coveralls-upload": "if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
"test:flat": "npm run test:flat:build && karma start test/flat.conf.js",
"test:flat:build": "npm run test:flat:build:ui && npm run test:flat:build:tests",
"test:flat:build:tests": "node test/integration/index.js",
"test:flat:build:states": "node development/genStates.js",
"test:flat:build:ui": "npm run test:flat:build:states && browserify ./mock-dev.js -o ./development/bundle.js",
"test:mascara": "npm run test:mascara:build && karma start test/mascara.conf.js",
"test:mascara:build": "mkdir -p dist/mascara && npm run test:mascara:build:ui && npm run test:mascara:build:background && npm run test:mascara:build:tests",
"test:mascara:build:ui": "browserify mascara/test/test-ui.js -o dist/mascara/ui.js",
"test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js",
"test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js",
"lint": "gulp lint",
"disc": "gulp disc --debug",
"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"
"mascara": "node ./mascara/example/server",
"buildMascaraCi": "browserify mascara/test/window-load.js -o mascara/test/bundle.js",
"buildMascaraSWCi": "browserify mascara/src/background.js -o mascara/test/background.js",
"mascaraCi": "npm run buildMascaraCi && npm run buildMascaraSWCi && node mascara/test/index.js",
"testMascara": "cd mascara/test && npm run mascaraCi && testem ci -P 3"
}, },
"browserify": { "browserify": {
"transform": [ "transform": [
@ -68,6 +71,7 @@
"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",
@ -92,12 +96,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.2.0",
"json-rpc-middleware-stream": "^1.0.1",
"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",
@ -118,7 +125,7 @@
"react-select": "^1.0.0-rc.2", "react-select": "^1.0.0-rc.2",
"react-simple-file-input": "^1.0.0", "react-simple-file-input": "^1.0.0",
"react-tooltip-component": "^0.3.0", "react-tooltip-component": "^0.3.0",
"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",
@ -170,7 +177,6 @@
"jsdom": "^11.1.0", "jsdom": "^11.1.0",
"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",
"karma": "^1.7.1", "karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0", "karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1", "karma-cli": "^1.0.1",

@ -2,7 +2,7 @@
// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT) // Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT)
module.exports = function(config) { module.exports = function(config) {
config.set({ return {
// base path that will be used to resolve all patterns (eg. files, exclude) // base path that will be used to resolve all patterns (eg. files, exclude)
basePath: process.cwd(), basePath: process.cwd(),
@ -16,9 +16,7 @@ module.exports = function(config) {
// list of files / patterns to load in the browser // list of files / patterns to load in the browser
files: [ files: [
'development/bundle.js',
'test/integration/jquery-3.1.0.min.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/images/**/*.*', watched: false, included: false, served: true },
{ pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true }, { pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true },
], ],
@ -57,5 +55,5 @@ module.exports = function(config) {
// Concurrency level // Concurrency level
// how many browser should be started simultaneous // how many browser should be started simultaneous
concurrency: Infinity concurrency: Infinity
}) }
} }

@ -0,0 +1,8 @@
const getBaseConfig = require('./base.conf.js')
module.exports = function(config) {
const settings = getBaseConfig(config)
settings.files.push('development/bundle.js')
settings.files.push('test/integration/bundle.js')
config.set(settings)
}

@ -10,19 +10,12 @@ QUnit.test('render init screen', (assert) => {
}) })
}) })
// QUnit.testDone(({ module, name, total, passed, failed, skipped, todo, runtime }) => {
// if (failed > 0) {
// const app = $('iframe').contents()[0].documentElement
// console.warn('Test failures - dumping DOM:')
// console.log(app.innerHTML)
// }
// })
async function runFirstTimeUsageTest(assert, done) { async function runFirstTimeUsageTest(assert, done) {
let waitTime = 0
if (window.METAMASK_PLATFORM_TYPE === 'mascara') waitTime = 4000
await timeout(waitTime)
await timeout() const app = $('#app-content')
const app = $('#app-content .mock-app-root')
// recurse notices // recurse notices
while (true) { while (true) {
@ -32,10 +25,12 @@ async function runFirstTimeUsageTest(assert, done) {
const termsPage = app.find('.markdown')[0] const termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight termsPage.scrollTop = termsPage.scrollHeight
await timeout() await timeout()
console.log('Clearing notice')
button.click() button.click()
await timeout() await timeout()
} else { } else {
// exit loop // exit loop
console.log('No more notices...')
break break
} }
} }
@ -58,7 +53,7 @@ async function runFirstTimeUsageTest(assert, done) {
const createButton = app.find('button.primary')[0] const createButton = app.find('button.primary')[0]
createButton.click() createButton.click()
await timeout(1500) await timeout(3000)
const 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')
@ -129,10 +124,8 @@ async function runFirstTimeUsageTest(assert, done) {
assert.ok(children2, 'All network options present') assert.ok(children2, 'All network options present')
} }
function timeout(time) { function timeout (time) {
return new Promise(function (resolve, reject) { return new Promise((resolve, reject) => {
setTimeout(function () { setTimeout(resolve, time || 1500)
resolve()
}, time * 3 || 1500)
}) })
} }

@ -0,0 +1,17 @@
const getBaseConfig = require('./base.conf.js')
module.exports = function(config) {
const settings = getBaseConfig(config)
// ui and tests
settings.files.push('dist/mascara/ui.js')
settings.files.push('dist/mascara/tests.js')
// service worker background
settings.files.push({ pattern: 'dist/mascara/background.js', watched: false, included: false, served: true }),
settings.proxies['/background.js'] = '/base/dist/mascara/background.js'
// use this to keep the browser open for debugging
settings.browserNoActivityTimeout = 10000000
config.set(settings)
}

@ -61,7 +61,7 @@ const actions = {
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
const container = document.querySelector('#app-content') const container = document.querySelector('#test-container')
// parse opts // parse opts
var store = configureStore(states[selectedView]) var store = configureStore(states[selectedView])
@ -72,7 +72,7 @@ render(
h(Selector, { actions, selectedKey: selectedView, states, store }), h(Selector, { actions, selectedKey: selectedView, states, store }),
h('.mock-app-root', { h('#app-content', {
style: { style: {
height: '500px', height: '500px',
width: '360px', width: '360px',

@ -52,7 +52,9 @@ PendingTx.prototype.render = function () {
const gas = txParams.gas const gas = txParams.gas
const gasBn = hexToBn(gas) const gasBn = hexToBn(gas)
const gasLimit = new BN(parseInt(blockGasLimit)) const gasLimit = new BN(parseInt(blockGasLimit))
const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
const safeGasLimit = safeGasLimitBN.toString(10)
// Gas Price // Gas Price
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
@ -66,6 +68,8 @@ PendingTx.prototype.render = function () {
const balanceBn = hexToBn(balance) const balanceBn = hexToBn(balance)
const insufficientBalance = balanceBn.lt(maxCost) const insufficientBalance = balanceBn.lt(maxCost)
const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
const gasLimitSpecified = txMeta.gasLimitSpecified
const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
const showRejectAll = props.unconfTxListLength > 1 const showRejectAll = props.unconfTxListLength > 1
@ -263,33 +267,44 @@ PendingTx.prototype.render = function () {
text-transform: uppercase; text-transform: uppercase;
} }
`), `),
h('.cell.row', {
style: {
textAlign: 'center',
},
}, [
txMeta.simulationFails ?
h('.error', {
style: {
fontSize: '0.9em',
},
}, 'Transaction Error. Exception thrown in contract code.')
: null,
txMeta.simulationFails ? !isValidAddress ?
h('.error', { h('.error', {
style: { style: {
marginLeft: 50, fontSize: '0.9em',
fontSize: '0.9em', },
}, }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
}, 'Transaction Error. Exception thrown in contract code.') : null,
: null,
!isValidAddress ? insufficientBalance ?
h('.error', { h('span.error', {
style: { style: {
marginLeft: 50, fontSize: '0.9em',
fontSize: '0.9em', },
}, }, 'Insufficient balance for transaction')
}, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') : null,
: null,
(dangerousGasLimit && !gasLimitSpecified) ?
h('span.error', {
style: {
fontSize: '0.9em',
},
}, 'Gas limit set dangerously high. Approving this transaction is likely to fail.')
: null,
]),
insufficientBalance ?
h('span.error', {
style: {
marginLeft: 50,
fontSize: '0.9em',
},
}, 'Insufficient balance for transaction')
: null,
// send + cancel // send + cancel
h('.flex-row.flex-space-around.conf-buttons', { h('.flex-row.flex-space-around.conf-buttons', {

@ -17,6 +17,6 @@ Tooltip.prototype.render = function () {
return h(ReactTooltip, { return h(ReactTooltip, {
position: position || 'left', position: position || 'left',
title, title,
fixed: false, fixed: true,
}, children) }, children)
} }

@ -35,7 +35,7 @@ TransactionIcon.prototype.render = function () {
case 'submitted': case 'submitted':
return h(Tooltip, { return h(Tooltip, {
title: 'Pending', title: 'Pending',
position: 'bottom', position: 'right',
}, [ }, [
h('i.fa.fa-ellipsis-h', { h('i.fa.fa-ellipsis-h', {
style: { style: {

@ -65,7 +65,7 @@ TransactionListItem.prototype.render = function () {
h(Tooltip, { h(Tooltip, {
title: 'Transaction Number', title: 'Transaction Number',
position: 'bottom', position: 'right',
}, [ }, [
h('span', { h('span', {
style: { style: {

@ -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: 'https://support.metamask.com', href: 'https://support.metamask.io',
target: '_blank', target: '_blank',
}, 'Visit our Support Center'), }, 'Visit our Support Center'),
]), ]),

@ -262,6 +262,11 @@ SendTransactionScreen.prototype.onSubmit = function () {
return this.props.dispatch(actions.displayWarning(message)) return this.props.dispatch(actions.displayWarning(message))
} }
if ((util.isInvalidChecksumAddress(recipient))) {
message = 'Recipient address checksum is invalid.'
return this.props.dispatch(actions.displayWarning(message))
}
if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
message = 'Recipient address is invalid.' message = 'Recipient address is invalid.'
return this.props.dispatch(actions.displayWarning(message)) return this.props.dispatch(actions.displayWarning(message))

@ -37,6 +37,7 @@ module.exports = {
bnTable: bnTable, bnTable: bnTable,
isHex: isHex, isHex: isHex,
exportAsFile: exportAsFile, exportAsFile: exportAsFile,
isInvalidChecksumAddress,
} }
function valuesFor (obj) { function valuesFor (obj) {
@ -66,6 +67,12 @@ function isValidAddress (address) {
return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
} }
function isInvalidChecksumAddress (address) {
var prefixed = ethUtil.addHexPrefix(address)
if (address === '0x0000000000000000000000000000000000000000') return false
return !isAllOneCase(prefixed) && !ethUtil.isValidChecksumAddress(prefixed) && ethUtil.isValidAddress(prefixed)
}
function isAllOneCase (address) { function isAllOneCase (address) {
if (!address) return true if (!address) return true
var lower = address.toLowerCase() var lower = address.toLowerCase()

Loading…
Cancel
Save