aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md13
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/background.js8
-rw-r--r--app/scripts/contentscript.js45
-rw-r--r--app/scripts/lib/createLoggerMiddleware.js15
-rw-r--r--app/scripts/lib/createOriginMiddleware.js9
-rw-r--r--app/scripts/lib/createProviderMiddleware.js13
-rw-r--r--app/scripts/lib/inpage-provider.js69
-rw-r--r--app/scripts/lib/obj-multiplex.js48
-rw-r--r--app/scripts/lib/pending-tx-tracker.js12
-rw-r--r--app/scripts/lib/port-stream.js16
-rw-r--r--app/scripts/lib/stream-utils.js24
-rw-r--r--app/scripts/metamask-controller.js91
-rw-r--r--circle.yml2
-rw-r--r--development/index.html4
-rw-r--r--development/test.html5
-rw-r--r--mascara/src/proxy.js3
-rw-r--r--mascara/src/ui.js49
-rw-r--r--mascara/test/index.html21
-rw-r--r--mascara/test/lib/first-time.js119
-rw-r--r--mascara/test/test-ui.js12
-rw-r--r--mascara/test/testem.yml13
-rw-r--r--mascara/test/window-load.js5
-rw-r--r--mock-dev.js5
-rw-r--r--package.json52
-rw-r--r--test/base.conf.js (renamed from karma.conf.js)6
-rw-r--r--test/flat.conf.js8
-rw-r--r--test/integration/lib/first-time.js27
-rw-r--r--test/mascara.conf.js17
-rw-r--r--ui-dev.js4
-rw-r--r--ui/app/components/pending-tx.js65
-rw-r--r--ui/app/components/tooltip.js2
-rw-r--r--ui/app/components/transaction-list-item-icon.js2
-rw-r--r--ui/app/components/transaction-list-item.js2
-rw-r--r--ui/app/send.js5
-rw-r--r--ui/app/util.js7
36 files changed, 367 insertions, 433 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f30c04985..3ff062cf8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,23 @@
## 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 seed words as a file.
- 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
diff --git a/app/manifest.json b/app/manifest.json
index bd25c1f6f..67fb543b9 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.10.0",
+ "version": "3.10.2",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/scripts/background.js b/app/scripts/background.js
index f077ca7a8..1b96d68b5 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -1,6 +1,8 @@
const urlUtil = require('url')
const endOfStream = require('end-of-stream')
const pipe = require('pump')
+const log = require('loglevel')
+const extension = require('extensionizer')
const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform')
const ExtensionPlatform = require('./platforms/extension')
@@ -9,13 +11,11 @@ const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
-const extension = require('extensionizer')
const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
-const log = require('loglevel')
window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
@@ -29,12 +29,12 @@ let popupIsOpen = false
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
// initialization flow
-initialize().catch(console.error)
+initialize().catch(log.error)
async function initialize () {
const initState = await loadStateFromPersistence()
await setupController(initState)
- console.log('MetaMask initialization complete.')
+ log.debug('MetaMask initialization complete.')
}
//
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index acacf5d4c..90a0f1f22 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -1,11 +1,12 @@
+const fs = require('fs')
+const path = require('path')
+const pump = require('pump')
const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
-const PortStream = require('./lib/port-stream.js')
-const ObjectMultiplex = require('./lib/obj-multiplex')
+const ObjectMultiplex = require('obj-multiplex')
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()
// Eventually this streaming injection could be replaced with:
@@ -50,22 +51,42 @@ function setupStreams () {
pageStream.pipe(pluginStream).pipe(pageStream)
// setup local multistream channels
- const mx = ObjectMultiplex()
- mx.on('error', console.error)
- mx.pipe(pageStream).pipe(mx)
- mx.pipe(pluginStream).pipe(mx)
+ const mux = new ObjectMultiplex()
+ pump(
+ mux,
+ pageStream,
+ mux,
+ (err) => logStreamDisconnectWarning('MetaMask Inpage', err)
+ )
+ pump(
+ mux,
+ pluginStream,
+ mux,
+ (err) => logStreamDisconnectWarning('MetaMask Background', err)
+ )
// connect ping stream
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
- const phishingStream = mx.createStream('phishing')
+ const phishingStream = mux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning)
// ignore unused channels (handled by background, inpage)
- mx.ignoreStream('provider')
- mx.ignoreStream('publicConfig')
+ mux.ignoreStream('provider')
+ 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 () {
diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js
new file mode 100644
index 000000000..b92a965de
--- /dev/null
+++ b/app/scripts/lib/createLoggerMiddleware.js
@@ -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()
+ })
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js
new file mode 100644
index 000000000..e1e097cc4
--- /dev/null
+++ b/app/scripts/lib/createOriginMiddleware.js
@@ -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()
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js
new file mode 100644
index 000000000..6dd192411
--- /dev/null
+++ b/app/scripts/lib/createProviderMiddleware.js
@@ -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()
+ })
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index 13888dc67..da75c4be2 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -1,8 +1,9 @@
-const pipe = require('pump')
-const StreamProvider = require('web3-stream-provider')
+const pump = require('pump')
+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 ObjectMultiplex = require('./obj-multiplex')
-const createRandomId = require('./random-id')
+const ObjectMultiplex = require('obj-multiplex')
module.exports = MetamaskInpageProvider
@@ -10,64 +11,46 @@ function MetamaskInpageProvider (connectionStream) {
const self = this
// setup connectionStream multiplexing
- var multiStream = self.multiStream = ObjectMultiplex()
- pipe(
+ const mux = self.mux = new ObjectMultiplex()
+ pump(
connectionStream,
- multiStream,
+ mux,
connectionStream,
(err) => logStreamDisconnectWarning('MetaMask', err)
)
// subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
- pipe(
- multiStream.createStream('publicConfig'),
+ pump(
+ mux.createStream('publicConfig'),
self.publicConfigStore,
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
)
// ignore phishing warning message (handled elsewhere)
- multiStream.ignoreStream('phishing')
+ mux.ignoreStream('phishing')
// connect to async provider
- const asyncProvider = self.asyncProvider = new StreamProvider()
- pipe(
- asyncProvider,
- multiStream.createStream('provider'),
- asyncProvider,
+ const streamMiddleware = createStreamMiddleware()
+ pump(
+ streamMiddleware.stream,
+ mux.createStream('provider'),
+ streamMiddleware.stream,
(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
// also remap ids inbound and outbound
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
const self = this
-
- // 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)
- })
+ self.rpcEngine.handle(payload, cb)
}
@@ -124,14 +107,6 @@ MetamaskInpageProvider.prototype.isMetaMask = true
// util
-function eachJsonMessage (payload, transformFn) {
- if (Array.isArray(payload)) {
- return payload.map(transformFn)
- } else {
- return transformFn(payload)
- }
-}
-
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js
deleted file mode 100644
index 0034febe0..000000000
--- a/app/scripts/lib/obj-multiplex.js
+++ /dev/null
@@ -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
-}
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index b90851b58..44e9d50fa 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -76,6 +76,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce
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 isKnownTx = (
@@ -88,6 +91,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
// other
|| errorMessage.includes('gateway timeout')
|| errorMessage.includes('nonce too low')
+ || txMeta.retryCount > 1
)
// ignore resubmit warnings, return early
if (isKnownTx) return
@@ -117,10 +121,12 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
// Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return
- // Increment a try counter.
- txMeta.retryCount++
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) {
diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js
index 607a9c9ed..648d88087 100644
--- a/app/scripts/lib/port-stream.js
+++ b/app/scripts/lib/port-stream.js
@@ -1,5 +1,6 @@
const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits
+const noop = function(){}
module.exports = PortDuplexStream
@@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) {
if (Buffer.isBuffer(msg)) {
delete msg._isBuffer
var data = new Buffer(msg)
- // console.log('PortDuplexStream - saw message as buffer', data)
this.push(data)
} else {
- // console.log('PortDuplexStream - saw message', msg)
this.push(msg)
}
}
PortDuplexStream.prototype._onDisconnect = function () {
- try {
- this.push(null)
- } catch (err) {
- this.emit('error', err)
- }
+ this.destroy()
}
// stream plumbing
@@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
if (Buffer.isBuffer(msg)) {
var data = msg.toJSON()
data._isBuffer = true
- // console.log('PortDuplexStream - sent message as buffer', data)
this._port.postMessage(data)
} else {
- // console.log('PortDuplexStream - sent message', msg)
this._port.postMessage(msg)
}
} catch (err) {
- // console.error(err)
return cb(new Error('PortDuplexStream - disconnected'))
}
cb()
}
-
-// util
-
-function noop () {}
diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js
index ba79990cc..8bb0b4f3c 100644
--- a/app/scripts/lib/stream-utils.js
+++ b/app/scripts/lib/stream-utils.js
@@ -1,6 +1,6 @@
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 = {
jsonParseStream: jsonParseStream,
@@ -23,14 +23,14 @@ function jsonStringifyStream () {
}
function setupMultiplex (connectionStream) {
- var mx = ObjectMultiplex()
- connectionStream.pipe(mx).pipe(connectionStream)
- endOfStream(mx, function (err) {
- if (err) console.error(err)
- })
- endOfStream(connectionStream, function (err) {
- if (err) console.error(err)
- mx.destroy()
- })
- return mx
+ const mux = new ObjectMultiplex()
+ pump(
+ connectionStream,
+ mux,
+ connectionStream,
+ (err) => {
+ if (err) console.error(err)
+ }
+ )
+ return mux
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index a007d6fc5..fef16c3a9 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1,12 +1,18 @@
const EventEmitter = require('events')
const extend = require('xtend')
const promiseToCallback = require('promise-to-callback')
-const pipe = require('pump')
+const pump = require('pump')
const Dnode = require('dnode')
const ObservableStore = require('obs-store')
const EthStore = require('./lib/eth-store')
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 KeyringController = require('./keyring-controller')
const NetworkController = require('./controllers/network')
@@ -24,8 +30,6 @@ const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
-const debounce = require('debounce')
-
const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter {
@@ -77,12 +81,13 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider
this.provider = this.initializeProvider()
+ this.blockTracker = this.provider
// eth data query tools
this.ethQuery = new EthQuery(this.provider)
this.ethStore = new EthStore({
provider: this.provider,
- blockTracker: this.provider,
+ blockTracker: this.blockTracker,
})
// key mgmt
@@ -109,7 +114,7 @@ module.exports = class MetamaskController extends EventEmitter {
getNetwork: this.networkController.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider,
- blockTracker: this.provider,
+ blockTracker: this.blockTracker,
ethQuery: this.ethQuery,
ethStore: this.ethStore,
})
@@ -337,36 +342,43 @@ module.exports = class MetamaskController extends EventEmitter {
setupUntrustedCommunication (connectionStream, originDomain) {
// Check if new connection is blacklisted
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)
return
}
// setup multiplexing
- const mx = setupMultiplex(connectionStream)
+ const mux = setupMultiplex(connectionStream)
// connect features
- this.setupProviderConnection(mx.createStream('provider'), originDomain)
- this.setupPublicConfig(mx.createStream('publicConfig'))
+ this.setupProviderConnection(mux.createStream('provider'), originDomain)
+ this.setupPublicConfig(mux.createStream('publicConfig'))
}
setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
- const mx = setupMultiplex(connectionStream)
+ const mux = setupMultiplex(connectionStream)
// connect features
- this.setupControllerConnection(mx.createStream('controller'))
- this.setupProviderConnection(mx.createStream('provider'), originDomain)
+ this.setupControllerConnection(mux.createStream('controller'))
+ this.setupProviderConnection(mux.createStream('provider'), originDomain)
}
sendPhishingWarning (connectionStream, hostname) {
- const mx = setupMultiplex(connectionStream)
- const phishingStream = mx.createStream('phishing')
+ const mux = setupMultiplex(connectionStream)
+ const phishingStream = mux.createStream('phishing')
phishingStream.write({ hostname })
}
setupControllerConnection (outStream) {
const api = this.getApi()
const dnode = Dnode(api)
- outStream.pipe(dnode).pipe(outStream)
+ pump(
+ outStream,
+ dnode,
+ outStream,
+ (err) => {
+ if (err) log.error(err)
+ }
+ )
dnode.on('remote', (remote) => {
// push updates to popup
const sendUpdate = remote.sendUpdate.bind(remote)
@@ -374,27 +386,42 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
- setupProviderConnection (outStream, originDomain) {
- streamIntoProvider(outStream, this.provider, onRequest, onResponse)
- // append dapp origin domain to request
- function onRequest (request) {
- request.origin = originDomain
- }
- // log rpc activity
- function onResponse (err, request, response) {
- if (err) return console.error(err)
- if (response.error) {
- console.error('Error in RPC response:\n', response)
+ setupProviderConnection (outStream, origin) {
+ // setup json rpc engine stack
+ const engine = new RpcEngine()
+
+ // create filter polyfill middleware
+ const filterMiddleware = createFilterMiddleware({
+ provider: this.provider,
+ blockTracker: this.blockTracker,
+ })
+
+ 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) {
- pipe(
+ pump(
this.publicConfigStore,
- outStream
+ outStream,
+ (err) => {
+ if (err) log.error(err)
+ }
)
}
diff --git a/circle.yml b/circle.yml
index f5da6857d..6aba5c1be 100644
--- a/circle.yml
+++ b/circle.yml
@@ -3,7 +3,7 @@ machine:
version: 8.1.4
test:
override:
- - "npm run ci"
+ - "npm test"
dependencies:
pre:
- sudo apt-get update
diff --git a/development/index.html b/development/index.html
index 048aa3f35..a0814cb55 100644
--- a/development/index.html
+++ b/development/index.html
@@ -14,13 +14,13 @@
</body>
<style>
-html, body, #app-content, .super-dev-container {
+html, body, #test-container, .super-dev-container {
height: 100%;
width: 100%;
position: relative;
background: white;
}
-.mock-app-root {
+#app-content {
background: #F7F7F7;
}
</style>
diff --git a/development/test.html b/development/test.html
index 702be7fa0..49084c0a4 100644
--- a/development/test.html
+++ b/development/test.html
@@ -18,13 +18,14 @@
</body>
<style>
-html, body, #app-content, .super-dev-container {
+html, body, #test-container, .super-dev-container {
height: 100%;
width: 100%;
position: relative;
background: white;
}
-.mock-app-root {
+
+#app-content {
background: #F7F7F7;
}
</style>
diff --git a/mascara/src/proxy.js b/mascara/src/proxy.js
index 5b95175f1..07c5b0e3c 100644
--- a/mascara/src/proxy.js
+++ b/mascara/src/proxy.js
@@ -1,7 +1,6 @@
const createParentStream = require('iframe-stream').ParentStream
const SWcontroller = require('client-sw-ready-event/lib/sw-client.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
const background = new SWcontroller({
@@ -12,7 +11,7 @@ const background = new SWcontroller({
})
const pageStream = createParentStream()
-background.on('ready', (_) => {
+background.on('ready', () => {
let swStream = SwStream({
serviceWorker: background.controller,
context: 'dapp',
diff --git a/mascara/src/ui.js b/mascara/src/ui.js
index 5f9be542f..2f940ad1a 100644
--- a/mascara/src/ui.js
+++ b/mascara/src/ui.js
@@ -2,8 +2,6 @@ const injectCss = require('inject-css')
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
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 startPopup = require('../../app/scripts/popup-core')
@@ -17,6 +15,7 @@ const container = document.getElementById('app-content')
var name = 'popup'
window.METAMASK_UI_TYPE = name
+window.METAMASK_PLATFORM_TYPE = 'mascara'
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
@@ -32,25 +31,39 @@ const connectApp = function (readSw) {
serviceWorker: background.controller,
context: name,
})
- startPopup({container, connectionStream}, (err, store) => {
- if (err) return displayCriticalError(err)
- store.subscribe(() => {
- const state = store.getState()
- if (state.appState.shouldClose) window.close()
+ return new Promise((resolve, reject) => {
+ startPopup({ container, connectionStream }, (err, store) => {
+ console.log('hello from MetaMascara ui!')
+ if (err) reject(err)
+ store.subscribe(() => {
+ const state = store.getState()
+ if (state.appState.shouldClose) window.close()
+ })
+ resolve()
})
})
}
-background.on('ready', (sw) => {
- background.removeListener('updatefound', connectApp)
- connectApp(sw)
+background.on('ready', async (sw) => {
+ try {
+ 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()
-.then(() => {
- setTimeout(() => {
- const appContent = document.getElementById(`app-content`)
- if (!appContent.children.length) window.location.reload()
- }, 2000)
-})
-console.log('hello from MetaMascara ui!')
+
+function windowReload() {
+ if (window.METAMASK_SKIP_RELOAD) return
+ window.location.reload()
+}
+
+function timeout (time) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, time || 1500)
+ })
+} \ No newline at end of file
diff --git a/mascara/test/index.html b/mascara/test/index.html
deleted file mode 100644
index 6495c2cfc..000000000
--- a/mascara/test/index.html
+++ /dev/null
@@ -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>
diff --git a/mascara/test/lib/first-time.js b/mascara/test/lib/first-time.js
deleted file mode 100644
index e42c9e39d..000000000
--- a/mascara/test/lib/first-time.js
+++ /dev/null
@@ -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()
- })
-})
diff --git a/mascara/test/test-ui.js b/mascara/test/test-ui.js
new file mode 100644
index 000000000..b9bc42dff
--- /dev/null
+++ b/mascara/test/test-ui.js
@@ -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')
+})
diff --git a/mascara/test/testem.yml b/mascara/test/testem.yml
deleted file mode 100644
index f1f5844bd..000000000
--- a/mascara/test/testem.yml
+++ /dev/null
@@ -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"
diff --git a/mascara/test/window-load.js b/mascara/test/window-load.js
deleted file mode 100644
index d3f44f05f..000000000
--- a/mascara/test/window-load.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const Helper = require('./util/mascara-test-helper.js')
-
-window.addEventListener('load', () => {
- require('../src/ui.js')
-})
diff --git a/mock-dev.js b/mock-dev.js
index b6652bdf7..a47f1ed4d 100644
--- a/mock-dev.js
+++ b/mock-dev.js
@@ -94,9 +94,8 @@ startApp()
function startApp(){
const body = document.body
const container = document.createElement('div')
- container.id = 'app-content'
+ container.id = 'test-container'
body.appendChild(container)
- console.log('container', container)
render(
h('.super-dev-container', [
@@ -113,7 +112,7 @@ function startApp(){
h(Selector, { actions, selectedKey: selectedView, states, store }),
- h('.mock-app-root', {
+ h('#app-content', {
style: {
height: '500px',
width: '360px',
diff --git a/package.json b/package.json
index 12f79ba35..0d02f1df5 100644
--- a/package.json
+++ b/package.json
@@ -6,30 +6,33 @@
"scripts": {
"start": "npm run dev",
"dev": "gulp dev --debug",
- "disc": "gulp disc --debug",
- "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 ./",
+ "ui": "npm run test:flat:build:states && 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 ./",
- "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",
"generateNotice": "node notices/notice-generator.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"
+ "deleteNotice": "node notices/notice-delete.js"
},
"browserify": {
"transform": [
@@ -68,6 +71,7 @@
"eth-bin-to-ops": "^1.0.1",
"eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.1.1",
+ "eth-json-rpc-filters": "^1.1.0",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.2.2",
@@ -92,12 +96,15 @@
"iframe-stream": "^3.0.0",
"inject-css": "^0.1.1",
"jazzicon": "^1.2.0",
+ "json-rpc-engine": "^3.2.0",
+ "json-rpc-middleware-stream": "^1.0.1",
"loglevel": "^1.4.1",
"metamask-logo": "^2.1.2",
"mississippi": "^1.2.0",
"mkdirp": "^0.5.1",
"multiplex": "^6.7.0",
"number-to-bn": "^1.7.0",
+ "obj-multiplex": "^1.0.0",
"obs-store": "^2.3.1",
"once": "^1.3.3",
"ping-pong-stream": "^1.0.0",
@@ -118,7 +125,7 @@
"react-select": "^1.0.0-rc.2",
"react-simple-file-input": "^1.0.0",
"react-tooltip-component": "^0.3.0",
- "readable-stream": "^2.1.2",
+ "readable-stream": "^2.3.3",
"redux": "^3.0.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
@@ -170,7 +177,6 @@
"jsdom": "^11.1.0",
"jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1",
- "json-rpc-engine": "^3.0.1",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1",
diff --git a/karma.conf.js b/test/base.conf.js
index 8e6d55972..122392822 100644
--- a/karma.conf.js
+++ b/test/base.conf.js
@@ -2,7 +2,7 @@
// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT)
module.exports = function(config) {
- config.set({
+ return {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: process.cwd(),
@@ -16,9 +16,7 @@ module.exports = function(config) {
// 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 },
],
@@ -57,5 +55,5 @@ module.exports = function(config) {
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
- })
+ }
}
diff --git a/test/flat.conf.js b/test/flat.conf.js
new file mode 100644
index 000000000..cd2dbdcdc
--- /dev/null
+++ b/test/flat.conf.js
@@ -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)
+}
diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js
index 38a94e551..cedb14f6e 100644
--- a/test/integration/lib/first-time.js
+++ b/test/integration/lib/first-time.js
@@ -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) {
+ let waitTime = 0
+ if (window.METAMASK_PLATFORM_TYPE === 'mascara') waitTime = 4000
+ await timeout(waitTime)
- await timeout()
-
- const app = $('#app-content .mock-app-root')
+ const app = $('#app-content')
// recurse notices
while (true) {
@@ -32,10 +25,12 @@ async function runFirstTimeUsageTest(assert, done) {
const termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
await timeout()
+ console.log('Clearing notice')
button.click()
await timeout()
} else {
// exit loop
+ console.log('No more notices...')
break
}
}
@@ -58,7 +53,7 @@ async function runFirstTimeUsageTest(assert, done) {
const createButton = app.find('button.primary')[0]
createButton.click()
- await timeout(1500)
+ await timeout(3000)
const created = app.find('h3')[0]
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')
}
-function timeout(time) {
- return new Promise(function (resolve, reject) {
- setTimeout(function () {
- resolve()
- }, time * 3 || 1500)
+function timeout (time) {
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, time || 1500)
})
} \ No newline at end of file
diff --git a/test/mascara.conf.js b/test/mascara.conf.js
new file mode 100644
index 000000000..97e53fc2b
--- /dev/null
+++ b/test/mascara.conf.js
@@ -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)
+}
diff --git a/ui-dev.js b/ui-dev.js
index 367b5d546..de5dfd8ef 100644
--- a/ui-dev.js
+++ b/ui-dev.js
@@ -61,7 +61,7 @@ const actions = {
var css = MetaMaskUiCss()
injectCss(css)
-const container = document.querySelector('#app-content')
+const container = document.querySelector('#test-container')
// parse opts
var store = configureStore(states[selectedView])
@@ -72,7 +72,7 @@ render(
h(Selector, { actions, selectedKey: selectedView, states, store }),
- h('.mock-app-root', {
+ h('#app-content', {
style: {
height: '500px',
width: '360px',
diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js
index 3e53d47f9..c3350fcc1 100644
--- a/ui/app/components/pending-tx.js
+++ b/ui/app/components/pending-tx.js
@@ -52,7 +52,9 @@ PendingTx.prototype.render = function () {
const gas = txParams.gas
const gasBn = hexToBn(gas)
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
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
@@ -66,6 +68,8 @@ PendingTx.prototype.render = function () {
const balanceBn = hexToBn(balance)
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 showRejectAll = props.unconfTxListLength > 1
@@ -263,33 +267,44 @@ PendingTx.prototype.render = function () {
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 ?
- h('.error', {
- style: {
- marginLeft: 50,
- fontSize: '0.9em',
- },
- }, 'Transaction Error. Exception thrown in contract code.')
- : null,
+ !isValidAddress ?
+ h('.error', {
+ style: {
+ fontSize: '0.9em',
+ },
+ }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
+ : 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: {
+ fontSize: '0.9em',
+ },
+ }, 'Insufficient balance for transaction')
+ : 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
h('.flex-row.flex-space-around.conf-buttons', {
diff --git a/ui/app/components/tooltip.js b/ui/app/components/tooltip.js
index edbc074bb..efab2c497 100644
--- a/ui/app/components/tooltip.js
+++ b/ui/app/components/tooltip.js
@@ -17,6 +17,6 @@ Tooltip.prototype.render = function () {
return h(ReactTooltip, {
position: position || 'left',
title,
- fixed: false,
+ fixed: true,
}, children)
}
diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js
index 431054340..f442b05af 100644
--- a/ui/app/components/transaction-list-item-icon.js
+++ b/ui/app/components/transaction-list-item-icon.js
@@ -35,7 +35,7 @@ TransactionIcon.prototype.render = function () {
case 'submitted':
return h(Tooltip, {
title: 'Pending',
- position: 'bottom',
+ position: 'right',
}, [
h('i.fa.fa-ellipsis-h', {
style: {
diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js
index 5d5d0bcc5..0e5c0b5a3 100644
--- a/ui/app/components/transaction-list-item.js
+++ b/ui/app/components/transaction-list-item.js
@@ -65,7 +65,7 @@ TransactionListItem.prototype.render = function () {
h(Tooltip, {
title: 'Transaction Number',
- position: 'bottom',
+ position: 'right',
}, [
h('span', {
style: {
diff --git a/ui/app/send.js b/ui/app/send.js
index a21a219eb..e59c1130e 100644
--- a/ui/app/send.js
+++ b/ui/app/send.js
@@ -262,6 +262,11 @@ SendTransactionScreen.prototype.onSubmit = function () {
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)) {
message = 'Recipient address is invalid.'
return this.props.dispatch(actions.displayWarning(message))
diff --git a/ui/app/util.js b/ui/app/util.js
index 1368ebf11..3f8b4dcc3 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -37,6 +37,7 @@ module.exports = {
bnTable: bnTable,
isHex: isHex,
exportAsFile: exportAsFile,
+ isInvalidChecksumAddress,
}
function valuesFor (obj) {
@@ -66,6 +67,12 @@ function isValidAddress (address) {
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) {
if (!address) return true
var lower = address.toLowerCase()