aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Finlay <dan@danfinlay.com>2017-10-13 00:59:28 +0800
committerDan Finlay <dan@danfinlay.com>2017-10-13 01:25:19 +0800
commitd0d082d70c1e256aeb70f90fcdc864aeca00aed4 (patch)
treea487d9bb888eebeba9c68d570cc55a5541384451
parent1cba6543a42561c6691736d58f45e97f4832912b (diff)
parent29ee33359e818525549b5241adb6f5903a054bba (diff)
downloadtangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.tar
tangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.tar.gz
tangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.tar.bz2
tangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.tar.lz
tangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.tar.xz
tangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.tar.zst
tangerine-wallet-browser-d0d082d70c1e256aeb70f90fcdc864aeca00aed4.zip
Merge branch 'master' into i1340-SynchronousInjection
-rw-r--r--CHANGELOG.md13
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/background.js3
-rw-r--r--app/scripts/controllers/network.js54
-rw-r--r--app/scripts/controllers/transactions.js4
-rw-r--r--app/scripts/lib/events-proxy.js10
-rw-r--r--app/scripts/lib/nodeify.js12
-rw-r--r--app/scripts/lib/nonce-tracker.js10
-rw-r--r--app/scripts/lib/obj-proxy.js19
-rw-r--r--app/scripts/lib/pending-tx-tracker.js29
-rw-r--r--app/scripts/lib/tx-state-manager.js8
-rw-r--r--app/scripts/lib/typed-message-manager.js123
-rw-r--r--app/scripts/metamask-controller.js123
-rw-r--r--gulpfile.js2
-rw-r--r--mascara/example/app.js28
-rw-r--r--mascara/example/app/index.html2
-rw-r--r--mascara/server/index.js8
-rw-r--r--mascara/server/util.js10
-rw-r--r--mascara/src/background.js98
-rw-r--r--mascara/src/proxy.js4
-rw-r--r--mascara/src/ui.js10
-rw-r--r--package.json10
-rw-r--r--test/integration/lib/first-time.js3
-rw-r--r--test/unit/components/bn-as-decimal-input-test.js38
-rw-r--r--test/unit/network-contoller-test.js25
-rw-r--r--test/unit/nodeify-test.js7
-rw-r--r--test/unit/nonce-tracker-test.js7
-rw-r--r--test/unit/pending-tx-test.js54
-rw-r--r--ui/app/account-detail.js4
-rw-r--r--ui/app/actions.js27
-rw-r--r--ui/app/add-token.js2
-rw-r--r--ui/app/app.js2
-rw-r--r--ui/app/components/bn-as-decimal-input.js11
-rw-r--r--ui/app/components/pending-tx.js6
-rw-r--r--ui/app/components/pending-typed-msg-details.js59
-rw-r--r--ui/app/components/pending-typed-msg.js46
-rw-r--r--ui/app/components/typed-message-renderer.js42
-rw-r--r--ui/app/conf-tx.js25
-rw-r--r--ui/app/reducers/app.js4
-rw-r--r--ui/index.js2
-rw-r--r--ui/lib/tx-helper.js13
41 files changed, 742 insertions, 217 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 069602915..65f75ee18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,20 @@
## Current Master
+- Fix bug where web3 API was sometimes injected after the page loaded.
+
+## 3.11.0 2017-10-11
+
+- Add support for new eth_signTypedData method per EIP 712.
+- Fix bug where some transactions would be shown as pending forever, even after successfully mined.
+- Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined.
+- Fix link to support article on token addresses.
+
+## 3.10.9 2017-10-5
+
+- Only rebrodcast transactions for a day not a days worth of blocks
- Remove Slack link from info page, since it is a big phishing target.
+- Stop computing balance based on pending transactions, to avoid edge case where users are unable to send transactions.
## 3.10.8 2017-9-28
diff --git a/app/manifest.json b/app/manifest.json
index 0fc43c7d4..a0f449c68 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.10.8",
+ "version": "3.11.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 195881e15..3e560d302 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -124,7 +124,8 @@ function setupController (initState) {
var unapprovedTxCount = controller.txController.getUnapprovedTxCount()
var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount
- var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs
+ var unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount
+ var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs
if (count) {
label = String(count)
}
diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js
index 0f9db4d53..64ed4b7c2 100644
--- a/app/scripts/controllers/network.js
+++ b/app/scripts/controllers/network.js
@@ -1,11 +1,12 @@
const assert = require('assert')
const EventEmitter = require('events')
-const createMetamaskProvider = require('web3-provider-engine/zero.js')
const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend')
const EthQuery = require('eth-query')
+const createEthRpcClient = require('eth-rpc-client')
const createEventEmitterProxy = require('../lib/events-proxy.js')
+const createObjectProxy = require('../lib/obj-proxy.js')
const RPC_ADDRESS_LIST = require('../config.js').network
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
@@ -17,7 +18,8 @@ module.exports = class NetworkController extends EventEmitter {
this.networkStore = new ObservableStore('loading')
this.providerStore = new ObservableStore(config.provider)
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
- this._proxy = createEventEmitterProxy()
+ this.providerProxy = createObjectProxy()
+ this.blockTrackerProxy = createEventEmitterProxy()
this.on('networkDidChange', this.lookupNetwork)
}
@@ -25,12 +27,11 @@ module.exports = class NetworkController extends EventEmitter {
initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams
const rpcUrl = this.getCurrentRpcAddress()
- this._configureStandardProvider({ rpcUrl })
- this._proxy.on('block', this._logBlock.bind(this))
- this._proxy.on('error', this.verifyNetwork.bind(this))
- this.ethQuery = new EthQuery(this._proxy)
+ this._configureStandardClient({ rpcUrl })
+ this.blockTrackerProxy.on('block', this._logBlock.bind(this))
+ this.blockTrackerProxy.on('error', this.verifyNetwork.bind(this))
+ this.ethQuery = new EthQuery(this.providerProxy)
this.lookupNetwork()
- return this._proxy
}
verifyNetwork () {
@@ -76,8 +77,10 @@ module.exports = class NetworkController extends EventEmitter {
assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`)
// skip if type already matches
if (type === this.getProviderConfig().type) return
+ // lookup rpcTarget for typecreateMetamaskProvider
const rpcTarget = this.getRpcAddressForType(type)
assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`)
+ // update connectioncreateMetamaskProvider
this.providerStore.updateState({ type, rpcTarget })
this._switchNetwork({ rpcUrl: rpcTarget })
}
@@ -97,32 +100,29 @@ module.exports = class NetworkController extends EventEmitter {
_switchNetwork (providerParams) {
this.setNetworkState('loading')
- this._configureStandardProvider(providerParams)
+ this._configureStandardClient(providerParams)
this.emit('networkDidChange')
}
- _configureStandardProvider(_providerParams) {
+ _configureStandardClient(_providerParams) {
const providerParams = extend(this._baseProviderParams, _providerParams)
- const provider = createMetamaskProvider(providerParams)
- this._setProvider(provider)
- }
-
- _setProvider (provider) {
- // collect old block tracker events
- const oldProvider = this._provider
- let blockTrackerHandlers
- if (oldProvider) {
- // capture old block handlers
- blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers
- // tear down
- oldProvider.removeAllListeners()
- oldProvider.stop()
+ const client = createEthRpcClient(providerParams)
+ this._setClient(client)
+ }
+
+ _setClient (newClient) {
+ // teardown old client
+ const oldClient = this._currentClient
+ if (oldClient) {
+ oldClient.blockTracker.stop()
+ // asyncEventEmitter lacks a "removeAllListeners" method
+ // oldClient.blockTracker.removeAllListeners
+ oldClient.blockTracker._events = {}
}
- // override block tracler
- provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers)
// set as new provider
- this._provider = provider
- this._proxy.setTarget(provider)
+ this._currentClient = newClient
+ this.providerProxy.setTarget(newClient.provider)
+ this.blockTrackerProxy.setTarget(newClient.blockTracker)
}
_logBlock (block) {
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index 94e04c429..d46dee230 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -46,6 +46,7 @@ module.exports = class TransactionController extends EventEmitter {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.nonceTracker = new NonceTracker({
provider: this.provider,
+ blockTracker: this.blockTracker,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: (address) => {
return this.txStateManager.getFilteredTxList({
@@ -59,9 +60,10 @@ module.exports = class TransactionController extends EventEmitter {
this.pendingTxTracker = new PendingTransactionTracker({
provider: this.provider,
nonceTracker: this.nonceTracker,
- retryLimit: 3500, // Retry 3500 blocks, or about 1 day.
+ retryTimePeriod: 86400000, // Retry 3500 blocks, or about 1 day.
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
+ getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
})
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js
index d1199a278..840b06b1a 100644
--- a/app/scripts/lib/events-proxy.js
+++ b/app/scripts/lib/events-proxy.js
@@ -1,6 +1,5 @@
-module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
+module.exports = function createEventEmitterProxy(eventEmitter, eventHandlers = {}) {
let target = eventEmitter
- const eventHandlers = listeners || {}
const proxy = new Proxy({}, {
get: (obj, name) => {
// intercept listeners
@@ -14,9 +13,12 @@ module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
return true
},
})
+ proxy.setTarget(eventEmitter)
+ return proxy
+
function setTarget (eventEmitter) {
target = eventEmitter
- // migrate listeners
+ // migrate eventHandlers
Object.keys(eventHandlers).forEach((name) => {
eventHandlers[name].forEach((handler) => target.on(name, handler))
})
@@ -26,6 +28,4 @@ module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
eventHandlers[name].push(handler)
target.on(name, handler)
}
- if (listeners) proxy.setTarget(eventEmitter)
- return proxy
} \ No newline at end of file
diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js
index 832d6c6d3..d24e92206 100644
--- a/app/scripts/lib/nodeify.js
+++ b/app/scripts/lib/nodeify.js
@@ -1,10 +1,18 @@
const promiseToCallback = require('promise-to-callback')
+const noop = function(){}
module.exports = function nodeify (fn, context) {
return function(){
const args = [].slice.call(arguments)
- const callback = args.pop()
- if (typeof callback !== 'function') throw new Error('callback is not a function')
+ const lastArg = args[args.length - 1]
+ const lastArgIsCallback = typeof lastArg === 'function'
+ let callback
+ if (lastArgIsCallback) {
+ callback = lastArg
+ args.pop()
+ } else {
+ callback = noop
+ }
promiseToCallback(fn.apply(context, args))(callback)
}
}
diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js
index 0029ac953..2af40a27f 100644
--- a/app/scripts/lib/nonce-tracker.js
+++ b/app/scripts/lib/nonce-tracker.js
@@ -4,8 +4,9 @@ const Mutex = require('await-semaphore').Mutex
class NonceTracker {
- constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
+ constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
+ this.blockTracker = blockTracker
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions
@@ -53,7 +54,7 @@ class NonceTracker {
}
async _getCurrentBlock () {
- const blockTracker = this._getBlockTracker()
+ const blockTracker = this.blockTracker
const currentBlock = blockTracker.getCurrentBlock()
if (currentBlock) return currentBlock
return await Promise((reject, resolve) => {
@@ -139,11 +140,6 @@ class NonceTracker {
return { name: 'local', nonce: highest, details: { startPoint, highest } }
}
- // this is a hotfix for the fact that the blockTracker will
- // change when the network changes
- _getBlockTracker () {
- return this.provider._blockTracker
- }
}
module.exports = NonceTracker
diff --git a/app/scripts/lib/obj-proxy.js b/app/scripts/lib/obj-proxy.js
new file mode 100644
index 000000000..29ca1269f
--- /dev/null
+++ b/app/scripts/lib/obj-proxy.js
@@ -0,0 +1,19 @@
+module.exports = function createObjectProxy(obj) {
+ let target = obj
+ const proxy = new Proxy({}, {
+ get: (obj, name) => {
+ // intercept setTarget
+ if (name === 'setTarget') return setTarget
+ return target[name]
+ },
+ set: (obj, name, value) => {
+ target[name] = value
+ return true
+ },
+ })
+ return proxy
+
+ function setTarget (obj) {
+ target = obj
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index 6f1601586..df504c126 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -22,9 +22,12 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
super()
this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
- this.retryLimit = config.retryLimit || Infinity
+ // default is one day
+ this.retryTimePeriod = config.retryTimePeriod || 86400000
this.getPendingTransactions = config.getPendingTransactions
+ this.getCompletedTransactions = config.getCompletedTransactions
this.publishTransaction = config.publishTransaction
+ this._checkPendingTxs()
}
// checks if a signed tx is in a block and
@@ -99,8 +102,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
async _resubmitTx (txMeta) {
- if (txMeta.retryCount > this.retryLimit) {
- const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`)
+ if (Date.now() > txMeta.time + this.retryTimePeriod) {
+ const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1)
+ const err = new Error(`Gave up submitting after ${hours} hours.`)
return this.emit('tx:failed', txMeta.id, err)
}
@@ -118,6 +122,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
async _checkPendingTx (txMeta) {
const txHash = txMeta.hash
const txId = txMeta.id
+
// extra check in case there was an uncaught error during the
// signature and submission process
if (!txHash) {
@@ -126,6 +131,15 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.emit('tx:failed', txId, noTxHashErr)
return
}
+
+ // If another tx with the same nonce is mined, set as failed.
+ const taken = await this._checkIfNonceIsTaken(txMeta)
+ if (taken) {
+ const nonceTakenErr = new Error('Another transaction with this nonce has been mined.')
+ nonceTakenErr.name = 'NonceTakenErr'
+ return this.emit('tx:failed', txId, nonceTakenErr)
+ }
+
// get latest transaction status
let txParams
try {
@@ -157,4 +171,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
nonceGlobalLock.releaseLock()
}
+
+ async _checkIfNonceIsTaken (txMeta) {
+ const completed = this.getCompletedTransactions()
+ const sameNonce = completed.filter((otherMeta) => {
+ return otherMeta.txParams.nonce === txMeta.txParams.nonce
+ })
+ return sameNonce.length > 0
+ }
+
}
diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js
index cf8117864..2250403f6 100644
--- a/app/scripts/lib/tx-state-manager.js
+++ b/app/scripts/lib/tx-state-manager.js
@@ -46,6 +46,12 @@ module.exports = class TransactionStateManger extends EventEmitter {
return this.getFilteredTxList(opts)
}
+ getConfirmedTransactions (address) {
+ const opts = { status: 'confirmed' }
+ if (address) opts.from = address
+ return this.getFilteredTxList(opts)
+ }
+
addTx (txMeta) {
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
@@ -242,4 +248,4 @@ module.exports = class TransactionStateManger extends EventEmitter {
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js
new file mode 100644
index 000000000..8b760790e
--- /dev/null
+++ b/app/scripts/lib/typed-message-manager.js
@@ -0,0 +1,123 @@
+const EventEmitter = require('events')
+const ObservableStore = require('obs-store')
+const createId = require('./random-id')
+const assert = require('assert')
+const sigUtil = require('eth-sig-util')
+
+
+module.exports = class TypedMessageManager extends EventEmitter {
+ constructor (opts) {
+ super()
+ this.memStore = new ObservableStore({
+ unapprovedTypedMessages: {},
+ unapprovedTypedMessagesCount: 0,
+ })
+ this.messages = []
+ }
+
+ get unapprovedTypedMessagesCount () {
+ return Object.keys(this.getUnapprovedMsgs()).length
+ }
+
+ getUnapprovedMsgs () {
+ return this.messages.filter(msg => msg.status === 'unapproved')
+ .reduce((result, msg) => { result[msg.id] = msg; return result }, {})
+ }
+
+ addUnapprovedMessage (msgParams) {
+ this.validateParams(msgParams)
+
+ log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
+ // create txData obj with parameters and meta data
+ var time = (new Date()).getTime()
+ var msgId = createId()
+ var msgData = {
+ id: msgId,
+ msgParams: msgParams,
+ time: time,
+ status: 'unapproved',
+ type: 'eth_signTypedData',
+ }
+ this.addMsg(msgData)
+
+ // signal update
+ this.emit('update')
+ return msgId
+ }
+
+ validateParams (params) {
+ assert.equal(typeof params, 'object', 'Params should ben an object.')
+ assert.ok('data' in params, 'Params must include a data field.')
+ assert.ok('from' in params, 'Params must include a from field.')
+ assert.ok(Array.isArray(params.data), 'Data should be an array.')
+ assert.equal(typeof params.from, 'string', 'From field must be a string.')
+ assert.doesNotThrow(() => {
+ sigUtil.typedSignatureHash(params.data)
+ }, 'Expected EIP712 typed data')
+ }
+
+ addMsg (msg) {
+ this.messages.push(msg)
+ this._saveMsgList()
+ }
+
+ getMsg (msgId) {
+ return this.messages.find(msg => msg.id === msgId)
+ }
+
+ approveMessage (msgParams) {
+ this.setMsgStatusApproved(msgParams.metamaskId)
+ return this.prepMsgForSigning(msgParams)
+ }
+
+ setMsgStatusApproved (msgId) {
+ this._setMsgStatus(msgId, 'approved')
+ }
+
+ setMsgStatusSigned (msgId, rawSig) {
+ const msg = this.getMsg(msgId)
+ msg.rawSig = rawSig
+ this._updateMsg(msg)
+ this._setMsgStatus(msgId, 'signed')
+ }
+
+ prepMsgForSigning (msgParams) {
+ delete msgParams.metamaskId
+ return Promise.resolve(msgParams)
+ }
+
+ rejectMsg (msgId) {
+ this._setMsgStatus(msgId, 'rejected')
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ _setMsgStatus (msgId, status) {
+ const msg = this.getMsg(msgId)
+ if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".')
+ msg.status = status
+ this._updateMsg(msg)
+ this.emit(`${msgId}:${status}`, msg)
+ if (status === 'rejected' || status === 'signed') {
+ this.emit(`${msgId}:finished`, msg)
+ }
+ }
+
+ _updateMsg (msg) {
+ const index = this.messages.findIndex((message) => message.id === msg.id)
+ if (index !== -1) {
+ this.messages[index] = msg
+ }
+ this._saveMsgList()
+ }
+
+ _saveMsgList () {
+ const unapprovedTypedMessages = this.getUnapprovedMsgs()
+ const unapprovedTypedMessagesCount = Object.keys(unapprovedTypedMessages).length
+ this.memStore.updateState({ unapprovedTypedMessages, unapprovedTypedMessagesCount })
+ this.emit('updateBadge')
+ }
+
+}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 1a468b6c7..a742f3cba 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -25,6 +25,7 @@ const InfuraController = require('./controllers/infura')
const BlacklistController = require('./controllers/blacklist')
const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager')
+const TypedMessageManager = require('./lib/typed-message-manager')
const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const ConfigManager = require('./lib/config-manager')
@@ -80,9 +81,24 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.blacklistController.scheduleUpdates()
- // rpc provider
- this.provider = this.initializeProvider()
- this.blockTracker = this.provider._blockTracker
+ // rpc provider and block tracker
+ this.networkController.initializeProvider({
+ scaffold: {
+ eth_syncing: false,
+ web3_clientVersion: `MetaMask/v${version}`,
+ },
+ // account mgmt
+ getAccounts: nodeify(this.getAccounts, this),
+ // tx signing
+ processTransaction: nodeify(this.newTransaction, this),
+ // old style msg signing
+ processMessage: this.newUnsignedMessage.bind(this),
+ // personal_sign msg signing
+ processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
+ processTypedMessage: this.newUnsignedTypedMessage.bind(this),
+ })
+ this.provider = this.networkController.providerProxy
+ this.blockTracker = this.networkController.blockTrackerProxy
// eth data query tools
this.ethQuery = new EthQuery(this.provider)
@@ -161,6 +177,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.lookupNetwork()
this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager()
+ this.typedMessageManager = new TypedMessageManager()
this.publicConfigStore = this.initPublicConfigStore()
// manual disk state subscriptions
@@ -202,6 +219,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.balancesController.store.subscribe(this.sendUpdate.bind(this))
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
+ this.typedMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
this.preferencesController.store.subscribe(this.sendUpdate.bind(this))
this.addressBookController.store.subscribe(this.sendUpdate.bind(this))
@@ -215,35 +233,6 @@ module.exports = class MetamaskController extends EventEmitter {
// Constructor helpers
//
- initializeProvider () {
- const providerOpts = {
- static: {
- eth_syncing: false,
- web3_clientVersion: `MetaMask/v${version}`,
- },
- // account mgmt
- getAccounts: (cb) => {
- const isUnlocked = this.keyringController.memStore.getState().isUnlocked
- const result = []
- const selectedAddress = this.preferencesController.getSelectedAddress()
-
- // only show address if account is unlocked
- if (isUnlocked && selectedAddress) {
- result.push(selectedAddress)
- }
- cb(null, result)
- },
- // tx signing
- processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this),
- // old style msg signing
- processMessage: this.newUnsignedMessage.bind(this),
- // personal_sign msg signing
- processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
- }
- const providerProxy = this.networkController.initializeProvider(providerOpts)
- return providerProxy
- }
-
initPublicConfigStore () {
// get init state
const publicConfigStore = new ObservableStore()
@@ -283,6 +272,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.txController.memStore.getState(),
this.messageManager.memStore.getState(),
this.personalMessageManager.memStore.getState(),
+ this.typedMessageManager.memStore.getState(),
this.keyringController.memStore.getState(),
this.balancesController.store.getState(),
this.preferencesController.store.getState(),
@@ -364,6 +354,10 @@ module.exports = class MetamaskController extends EventEmitter {
signPersonalMessage: nodeify(this.signPersonalMessage, this),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this),
+ // personalMessageManager
+ signTypedMessage: nodeify(this.signTypedMessage, this),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this),
+
// notices
checkNotices: noticeController.updateNoticesList.bind(noticeController),
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
@@ -474,6 +468,18 @@ module.exports = class MetamaskController extends EventEmitter {
// Opinionated Keyring Management
//
+ async getAccounts () {
+ const isUnlocked = this.keyringController.memStore.getState().isUnlocked
+ const result = []
+ const selectedAddress = this.preferencesController.getSelectedAddress()
+
+ // only show address if account is unlocked
+ if (isUnlocked && selectedAddress) {
+ result.push(selectedAddress)
+ }
+ return result
+ }
+
addNewAccount (cb) {
const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0]
if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found'))
@@ -520,6 +526,11 @@ module.exports = class MetamaskController extends EventEmitter {
// Identity Management
//
+ // this function wrappper lets us pass the fn reference before txController is instantiated
+ async newTransaction (txParams) {
+ return await this.txController.newUnapprovedTransaction(txParams)
+ }
+
newUnsignedMessage (msgParams, cb) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
this.sendUpdate()
@@ -556,6 +567,28 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
+ newUnsignedTypedMessage (msgParams, cb) {
+ let msgId
+ try {
+ msgId = this.typedMessageManager.addUnapprovedMessage(msgParams)
+ this.sendUpdate()
+ this.opts.showUnconfirmedMessage()
+ } catch (e) {
+ return cb(e)
+ }
+
+ this.typedMessageManager.once(`${msgId}:finished`, (data) => {
+ switch (data.status) {
+ case 'signed':
+ return cb(null, data.rawSig)
+ case 'rejected':
+ return cb(new Error('MetaMask Message Signature: User denied message signature.'))
+ default:
+ return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
+ }
+ })
+ }
+
signMessage (msgParams, cb) {
log.info('MetaMaskController - signMessage')
const msgId = msgParams.metamaskId
@@ -618,6 +651,24 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
+ signTypedMessage (msgParams) {
+ log.info('MetaMaskController - signTypedMessage')
+ const msgId = msgParams.metamaskId
+ // sets the status op the message to 'approved'
+ // and removes the metamaskId for signing
+ return this.typedMessageManager.approveMessage(msgParams)
+ .then((cleanMsgParams) => {
+ // signs the message
+ return this.keyringController.signTypedMessage(cleanMsgParams)
+ })
+ .then((rawSig) => {
+ // tells the listener that the message has been signed
+ // and can be returned to the dapp
+ this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
+ return this.getState()
+ })
+ }
+
cancelPersonalMessage (msgId, cb) {
const messageManager = this.personalMessageManager
messageManager.rejectMsg(msgId)
@@ -626,6 +677,14 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
+ cancelTypedMessage (msgId, cb) {
+ const messageManager = this.typedMessageManager
+ messageManager.rejectMsg(msgId)
+ if (cb && typeof cb === 'function') {
+ cb(null, this.getState())
+ }
+ }
+
markAccountsFound (cb) {
this.configManager.setLostAccounts([])
this.sendUpdate()
diff --git a/gulpfile.js b/gulpfile.js
index 14e26ed2e..6d2ff5fdd 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -151,7 +151,7 @@ gulp.task('copy:watch', function(){
gulp.task('lint', function () {
// Ignoring node_modules, dist/firefox, and docs folders:
- return gulp.src(['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js'])
+ return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js'])
.pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc'))))
// eslint.format() outputs the lint results to the console.
// Alternatively use eslint.formatEach() (see Docs).
diff --git a/mascara/example/app.js b/mascara/example/app.js
index d0cb6ba83..598e2c84c 100644
--- a/mascara/example/app.js
+++ b/mascara/example/app.js
@@ -7,20 +7,32 @@ async function loadProvider() {
const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' })
const ethQuery = new EthQuery(ethereumProvider)
const accounts = await ethQuery.accounts()
- logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
- setupButton(ethQuery)
+ window.METAMASK_ACCOUNT = accounts[0] || 'locked'
+ logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account')
+ setupButtons(ethQuery)
}
-function logToDom(message){
- document.getElementById('account').innerText = message
+function logToDom(message, context){
+ document.getElementById(context).innerText = message
console.log(message)
}
-function setupButton (ethQuery) {
- const button = document.getElementById('action-button-1')
- button.addEventListener('click', async () => {
+function setupButtons (ethQuery) {
+ const accountButton = document.getElementById('action-button-1')
+ accountButton.addEventListener('click', async () => {
const accounts = await ethQuery.accounts()
- logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
+ window.METAMASK_ACCOUNT = accounts[0] || 'locked'
+ logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account')
+ })
+ const txButton = document.getElementById('action-button-2')
+ txButton.addEventListener('click', async () => {
+ if (!window.METAMASK_ACCOUNT || window.METAMASK_ACCOUNT === 'locked') return
+ const txHash = await ethQuery.sendTransaction({
+ from: window.METAMASK_ACCOUNT,
+ to: window.METAMASK_ACCOUNT,
+ data: '',
+ })
+ logToDom(txHash, 'cb-value')
})
} \ No newline at end of file
diff --git a/mascara/example/app/index.html b/mascara/example/app/index.html
index f3e38877c..8afb6f3f2 100644
--- a/mascara/example/app/index.html
+++ b/mascara/example/app/index.html
@@ -10,6 +10,8 @@
<body>
<button id="action-button-1">GET ACCOUNT</button>
<div id="account"></div>
+ <button id="action-button-2">SEND TRANSACTION</button>
+ <div id="cb-value" ></div>
<script src="./app.js"></script>
</body>
</html> \ No newline at end of file
diff --git a/mascara/server/index.js b/mascara/server/index.js
index 14e3fa18e..12b527e5d 100644
--- a/mascara/server/index.js
+++ b/mascara/server/index.js
@@ -5,7 +5,7 @@ const serveBundle = require('./util').serveBundle
module.exports = createMetamascaraServer
-function createMetamascaraServer(){
+function createMetamascaraServer () {
// start bundlers
const metamascaraBundle = createBundle(__dirname + '/../src/mascara.js')
@@ -17,13 +17,13 @@ function createMetamascaraServer(){
const server = express()
// ui window
serveBundle(server, '/ui.js', uiBundle)
- server.use(express.static(__dirname+'/../ui/'))
- server.use(express.static(__dirname+'/../../dist/chrome'))
+ server.use(express.static(__dirname + '/../ui/'))
+ server.use(express.static(__dirname + '/../../dist/chrome'))
// metamascara
serveBundle(server, '/metamascara.js', metamascaraBundle)
// proxy
serveBundle(server, '/proxy/proxy.js', proxyBundle)
- server.use('/proxy/', express.static(__dirname+'/../proxy'))
+ server.use('/proxy/', express.static(__dirname + '/../proxy'))
// background
serveBundle(server, '/background.js', backgroundBuild)
diff --git a/mascara/server/util.js b/mascara/server/util.js
index 6e25b35d8..6ab41b729 100644
--- a/mascara/server/util.js
+++ b/mascara/server/util.js
@@ -7,14 +7,14 @@ module.exports = {
}
-function serveBundle(server, path, bundle){
- server.get(path, function(req, res){
+function serveBundle (server, path, bundle) {
+ server.get(path, function (req, res) {
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
res.send(bundle.latest)
})
}
-function createBundle(entryPoint){
+function createBundle (entryPoint) {
var bundleContainer = {}
@@ -30,8 +30,8 @@ function createBundle(entryPoint){
return bundleContainer
- function bundle() {
- bundler.bundle(function(err, result){
+ function bundle () {
+ bundler.bundle(function (err, result) {
if (err) {
console.log(`Bundle failed! (${entryPoint})`)
console.error(err)
diff --git a/mascara/src/background.js b/mascara/src/background.js
index 5ba865ad8..8aa1d8fe2 100644
--- a/mascara/src/background.js
+++ b/mascara/src/background.js
@@ -1,72 +1,60 @@
global.window = global
-const self = global
-const pipe = require('pump')
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
-const connectionListener = new SwGlobalListener(self)
+const connectionListener = new SwGlobalListener(global)
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
-const PortStream = require('../../app/scripts/lib/port-stream.js')
const DbController = require('idb-global')
const SwPlatform = require('../../app/scripts/platforms/sw')
const MetamaskController = require('../../app/scripts/metamask-controller')
-const extension = {} //require('../../app/scripts/lib/extension')
-const storeTransform = require('obs-store/lib/transform')
const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = require('../../app/scripts/migrations/')
const firstTimeState = require('../../app/scripts/first-time-state')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
-let popupIsOpen = false
-let connectedClientCount = 0
+global.metamaskPopupIsOpen = false
const log = require('loglevel')
global.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
-self.addEventListener('install', function(event) {
- event.waitUntil(self.skipWaiting())
+global.addEventListener('install', function (event) {
+ event.waitUntil(global.skipWaiting())
})
-self.addEventListener('activate', function(event) {
- event.waitUntil(self.clients.claim())
+global.addEventListener('activate', function (event) {
+ event.waitUntil(global.clients.claim())
})
-console.log('inside:open')
+log.debug('inside:open')
// // state persistence
-let diskStore
const dbController = new DbController({
key: STORAGE_KEY,
})
loadStateFromPersistence()
.then((initState) => setupController(initState))
-.then(() => console.log('MetaMask initialization complete.'))
+.then(() => log.debug('MetaMask initialization complete.'))
.catch((err) => console.error('WHILE SETTING UP:', err))
-// initialization flow
-
//
// State and Persistence
//
-function loadStateFromPersistence() {
+async function loadStateFromPersistence () {
// migrations
- let migrator = new Migrator({ migrations })
+ const migrator = new Migrator({ migrations })
const initialState = migrator.generateInitialState(firstTimeState)
dbController.initialState = initialState
- return dbController.open()
- .then((versionedData) => migrator.migrateData(versionedData))
- .then((versionedData) => {
- dbController.put(versionedData)
- return Promise.resolve(versionedData)
- })
- .then((versionedData) => Promise.resolve(versionedData.data))
+ const versionedData = await dbController.open()
+ const migratedData = await migrator.migrateData(versionedData)
+ await dbController.put(migratedData)
+ return migratedData.data
}
-function setupController (initState, client) {
+async function setupController (initState, client) {
//
// MetaMask Controller
@@ -86,19 +74,19 @@ function setupController (initState, client) {
})
global.metamaskController = controller
- controller.store.subscribe((state) => {
- versionifyData(state)
- .then((versionedData) => dbController.put(versionedData))
- .catch((err) => {console.error(err)})
+ controller.store.subscribe(async (state) => {
+ try {
+ const versionedData = await versionifyData(state)
+ await dbController.put(versionedData)
+ } catch (e) { console.error('METAMASK Error:', e) }
})
- function versionifyData(state) {
- return dbController.get()
- .then((rawData) => {
- return Promise.resolve({
- data: state,
- meta: rawData.meta,
- })}
- )
+
+ async function versionifyData (state) {
+ const rawData = await dbController.get()
+ return {
+ data: state,
+ meta: rawData.meta,
+ }
}
//
@@ -106,8 +94,7 @@ function setupController (initState, client) {
//
connectionListener.on('remote', (portStream, messageEvent) => {
- console.log('REMOTE CONECTION FOUND***********')
- connectedClientCount += 1
+ log.debug('REMOTE CONECTION FOUND***********')
connectRemote(portStream, messageEvent.data.context)
})
@@ -116,7 +103,7 @@ function setupController (initState, client) {
if (isMetaMaskInternalProcess) {
// communication with popup
controller.setupTrustedCommunication(connectionStream, 'MetaMask')
- popupIsOpen = true
+ global.metamaskPopupIsOpen = true
} else {
// communication with page
setupUntrustedCommunication(connectionStream, context)
@@ -130,25 +117,14 @@ function setupController (initState, client) {
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
-
- function setupTrustedCommunication (connectionStream, originDomain) {
- // setup multiplexing
- var mx = setupMultiplex(connectionStream)
- // connect features
- controller.setupProviderConnection(mx.createStream('provider'), originDomain)
- }
- //
- // User Interface setup
- //
- return Promise.resolve()
-
}
+// // this will be useful later but commented out for linting for now (liiiinting)
+// function sendMessageToAllClients (message) {
+// global.clients.matchAll().then(function (clients) {
+// clients.forEach(function (client) {
+// client.postMessage(message)
+// })
+// })
+// }
-function sendMessageToAllClients (message) {
- self.clients.matchAll().then(function(clients) {
- clients.forEach(function(client) {
- client.postMessage(message)
- })
- })
-}
function noop () {}
diff --git a/mascara/src/proxy.js b/mascara/src/proxy.js
index 07c5b0e3c..54c5d5cf4 100644
--- a/mascara/src/proxy.js
+++ b/mascara/src/proxy.js
@@ -2,7 +2,7 @@ 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')
-let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
+const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
@@ -12,7 +12,7 @@ const background = new SWcontroller({
const pageStream = createParentStream()
background.on('ready', () => {
- let swStream = SwStream({
+ const swStream = SwStream({
serviceWorker: background.controller,
context: 'dapp',
})
diff --git a/mascara/src/ui.js b/mascara/src/ui.js
index 2f940ad1a..b272a2e06 100644
--- a/mascara/src/ui.js
+++ b/mascara/src/ui.js
@@ -17,17 +17,17 @@ var name = 'popup'
window.METAMASK_UI_TYPE = name
window.METAMASK_PLATFORM_TYPE = 'mascara'
-let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
+const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
intervalDelay,
- wakeUpInterval: 20000
+ wakeUpInterval: 20000,
})
// Setup listener for when the service worker is read
const connectApp = function (readSw) {
- let connectionStream = SwStream({
+ const connectionStream = SwStream({
serviceWorker: background.controller,
context: name,
})
@@ -57,7 +57,7 @@ background.on('updatefound', windowReload)
background.startWorker()
-function windowReload() {
+function windowReload () {
if (window.METAMASK_SKIP_RELOAD) return
window.location.reload()
}
@@ -66,4 +66,4 @@ function timeout (time) {
return new Promise((resolve) => {
setTimeout(resolve, time || 1500)
})
-} \ No newline at end of file
+}
diff --git a/package.json b/package.json
index 03d095228..2b7b2056a 100644
--- a/package.json
+++ b/package.json
@@ -69,12 +69,14 @@
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.2.0",
"eth-contract-metadata": "^1.1.4",
- "eth-hd-keyring": "^1.1.1",
+ "eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.2",
- "eth-keyring-controller": "^2.0.0",
+ "eth-json-rpc-middleware": "^1.4.3",
+ "eth-keyring-controller": "^2.1.0",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
- "eth-sig-util": "^1.2.2",
+ "eth-rpc-client": "^1.1.3",
+ "eth-sig-util": "^1.4.0",
"eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.1.4",
"ethereumjs-tx": "^1.3.0",
@@ -141,7 +143,7 @@
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
- "web3-provider-engine": "^13.3.1",
+ "web3-provider-engine": "^13.3.2",
"web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1"
},
diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js
index cedb14f6e..ee49d0901 100644
--- a/test/integration/lib/first-time.js
+++ b/test/integration/lib/first-time.js
@@ -3,6 +3,9 @@ const PASSWORD = 'password123'
QUnit.module('first time usage')
QUnit.test('render init screen', (assert) => {
+ // intercept reload attempts
+ window.onbeforeunload = () => true
+
const done = assert.async()
runFirstTimeUsageTest(assert).then(done).catch((err) => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js
index 106b3a871..58ecc9c89 100644
--- a/test/unit/components/bn-as-decimal-input-test.js
+++ b/test/unit/components/bn-as-decimal-input-test.js
@@ -48,4 +48,42 @@ describe('BnInput', function () {
checkValidity () { return true } },
})
})
+
+ it('can tolerate wei precision', function (done) {
+ const renderer = ReactTestUtils.createRenderer()
+
+ let valueStr = '1000000000'
+
+ const value = new BN(valueStr, 10)
+ const inputStr = '1.000000001'
+
+
+ let targetStr = '1000000001'
+
+ const target = new BN(targetStr, 10)
+
+ const precision = 9 // gwei precision
+ const scale = 9
+
+ const props = {
+ value,
+ scale,
+ precision,
+ onChange: (newBn) => {
+ assert.equal(newBn.toString(), target.toString(), 'should tolerate increase')
+ const reInput = BnInput.prototype.downsize(newBn.toString(), 9, 9)
+ assert.equal(reInput.toString(), inputStr, 'should tolerate increase')
+ done()
+ },
+ }
+
+ const inputComponent = h(BnInput, props)
+ const component = additions.renderIntoDocument(inputComponent)
+ renderer.render(inputComponent)
+ const input = additions.find(component, 'input.hex-input')[0]
+ ReactTestUtils.Simulate.change(input, { preventDefault () {}, target: {
+ value: inputStr,
+ checkValidity () { return true } },
+ })
+ })
})
diff --git a/test/unit/network-contoller-test.js b/test/unit/network-contoller-test.js
index 0b3b5adeb..42ca40c56 100644
--- a/test/unit/network-contoller-test.js
+++ b/test/unit/network-contoller-test.js
@@ -14,15 +14,15 @@ describe('# Network Controller', function () {
},
})
- networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
+ networkController.initializeProvider(networkControllerProviderInit)
})
describe('network', function () {
describe('#provider', function () {
it('provider should be updatable without reassignment', function () {
- networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
- const proxy = networkController._proxy
- proxy.setTarget({ test: true, on: () => {} })
- assert.ok(proxy.test)
+ networkController.initializeProvider(networkControllerProviderInit)
+ const providerProxy = networkController.providerProxy
+ providerProxy.setTarget({ test: true })
+ assert.ok(providerProxy.test)
})
})
describe('#getNetworkState', function () {
@@ -66,19 +66,4 @@ describe('# Network Controller', function () {
})
})
-function dummyProviderConstructor() {
- return {
- // provider
- sendAsync: noop,
- // block tracker
- _blockTracker: {},
- start: noop,
- stop: noop,
- on: noop,
- addListener: noop,
- once: noop,
- removeAllListeners: noop,
- }
-}
-
function noop() {} \ No newline at end of file
diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js
index 537dae605..c7b127889 100644
--- a/test/unit/nodeify-test.js
+++ b/test/unit/nodeify-test.js
@@ -18,14 +18,13 @@ describe('nodeify', function () {
})
})
- it('should throw if the last argument is not a function', function (done) {
+ it('should allow the last argument to not be a function', function (done) {
const nodified = nodeify(obj.promiseFunc, obj)
try {
nodified('baz')
- done(new Error('should have thrown if the last argument is not a function'))
- } catch (err) {
- assert.equal(err.message, 'callback is not a function')
done()
+ } catch (err) {
+ done(new Error('should not have thrown if the last argument is not a function'))
}
})
})
diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js
index 8970cf84d..77af2a21c 100644
--- a/test/unit/nonce-tracker-test.js
+++ b/test/unit/nonce-tracker-test.js
@@ -190,12 +190,13 @@ function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
providerResultStub.result = providerStub
const provider = {
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
- _blockTracker: {
- getCurrentBlock: () => '0x11b568',
- },
+ }
+ const blockTracker = {
+ getCurrentBlock: () => '0x11b568',
}
return new NonceTracker({
provider,
+ blockTracker,
getPendingTransactions,
getConfirmedTransactions,
})
diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js
index 6b62bb5b1..32421a44f 100644
--- a/test/unit/pending-tx-test.js
+++ b/test/unit/pending-tx-test.js
@@ -5,6 +5,8 @@ const ObservableStore = require('obs-store')
const clone = require('clone')
const { createStubedProvider } = require('../stub/provider')
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker')
+const MockTxGen = require('../lib/mock-tx-gen')
+const sinon = require('sinon')
const noop = () => true
const currentNetworkId = 42
const otherNetworkId = 36
@@ -46,10 +48,60 @@ describe('PendingTransactionTracker', function () {
}
},
getPendingTransactions: () => {return []},
+ getCompletedTransactions: () => {return []},
publishTransaction: () => {},
})
})
+ describe('_checkPendingTx state management', function () {
+ let stub
+
+ afterEach(function () {
+ if (stub) {
+ stub.restore()
+ }
+ })
+
+ it('should become failed if another tx with the same nonce succeeds', async function () {
+
+ // SETUP
+ const txGen = new MockTxGen()
+
+ txGen.generate({
+ id: '456',
+ value: '0x01',
+ hash: '0xbad',
+ status: 'confirmed',
+ nonce: '0x01',
+ }, { count: 1 })
+
+ const pending = txGen.generate({
+ id: '123',
+ value: '0x02',
+ hash: '0xfad',
+ status: 'submitted',
+ nonce: '0x01',
+ }, { count: 1 })[0]
+
+ stub = sinon.stub(pendingTxTracker, 'getCompletedTransactions')
+ .returns(txGen.txs)
+
+ // THE EXPECTATION
+ const spy = sinon.spy()
+ pendingTxTracker.on('tx:failed', (txId, err) => {
+ assert.equal(txId, pending.id, 'should fail the pending tx')
+ assert.equal(err.name, 'NonceTakenErr', 'should emit a nonce taken error.')
+ spy(txId, err)
+ })
+
+ // THE METHOD
+ await pendingTxTracker._checkPendingTx(pending)
+
+ // THE ASSERTION
+ assert.ok(spy.calledWith(pending.id), 'tx failed should be emitted')
+ })
+ })
+
describe('#checkForTxInBlock', function () {
it('should return if no pending transactions', function () {
// throw a type error if it trys to do anything on the block
@@ -239,4 +291,4 @@ describe('PendingTransactionTracker', function () {
})
})
})
-}) \ No newline at end of file
+})
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js
index 90724dc3f..a844daf88 100644
--- a/ui/app/account-detail.js
+++ b/ui/app/account-detail.js
@@ -46,7 +46,7 @@ AccountDetailScreen.prototype.render = function () {
var selected = props.address || Object.keys(props.accounts)[0]
var checksumAddress = selected && ethUtil.toChecksumAddress(selected)
var identity = props.identities[selected]
- var account = props.computedBalances[selected]
+ var account = props.accounts[selected]
const { network, conversionRate, currentCurrency } = props
return (
@@ -181,7 +181,7 @@ AccountDetailScreen.prototype.render = function () {
}, [
h(EthBalance, {
- value: account && account.ethBalance,
+ value: account && account.balance,
conversionRate,
currentCurrency,
style: {
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 3ea092e57..84990922e 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -97,6 +97,8 @@ var actions = {
cancelMsg: cancelMsg,
signPersonalMsg,
cancelPersonalMsg,
+ signTypedMsg,
+ cancelTypedMsg,
signTx: signTx,
updateAndApproveTx,
cancelTx: cancelTx,
@@ -392,6 +394,25 @@ function signPersonalMsg (msgData) {
}
}
+function signTypedMsg (msgData) {
+ log.debug('action - signTypedMsg')
+ return (dispatch) => {
+ dispatch(actions.showLoadingIndication())
+
+ log.debug(`actions calling background.signTypedMessage`)
+ background.signTypedMessage(msgData, (err, newState) => {
+ log.debug('signTypedMessage called back')
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.hideLoadingIndication())
+
+ if (err) log.error(err)
+ if (err) return dispatch(actions.displayWarning(err.message))
+
+ dispatch(actions.completedTx(msgData.metamaskId))
+ })
+ }
+}
+
function signTx (txData) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@@ -446,6 +467,12 @@ function cancelPersonalMsg (msgData) {
return actions.completedTx(id)
}
+function cancelTypedMsg (msgData) {
+ const id = msgData.id
+ background.cancelTypedMessage(id)
+ return actions.completedTx(id)
+}
+
function cancelTx (txData) {
return (dispatch) => {
log.debug(`background.cancelTransaction`)
diff --git a/ui/app/add-token.js b/ui/app/add-token.js
index 18adc7eb5..9354a4cad 100644
--- a/ui/app/add-token.js
+++ b/ui/app/add-token.js
@@ -73,7 +73,7 @@ AddTokenScreen.prototype.render = function () {
}, [
h('a', {
style: { fontWeight: 'bold', paddingRight: '10px'},
- href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address',
+ href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
target: '_blank',
}, [
h('span', 'Token Contract Address '),
diff --git a/ui/app/app.js b/ui/app/app.js
index 613577913..30d3766ab 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -319,7 +319,7 @@ App.prototype.renderNetworkDropdown = function () {
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Localhost 8545',
- activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null,
+ providerType === 'localhost' ? h('.check', '✓') : null,
]
),
diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js
index f3ace4720..d84834d06 100644
--- a/ui/app/components/bn-as-decimal-input.js
+++ b/ui/app/components/bn-as-decimal-input.js
@@ -31,7 +31,7 @@ BnAsDecimalInput.prototype.render = function () {
const suffix = props.suffix
const style = props.style
const valueString = value.toString(10)
- const newValue = this.downsize(valueString, scale, precision)
+ const newValue = this.downsize(valueString, scale)
return (
h('.flex-column', [
@@ -145,14 +145,17 @@ BnAsDecimalInput.prototype.constructWarning = function () {
}
-BnAsDecimalInput.prototype.downsize = function (number, scale, precision) {
+BnAsDecimalInput.prototype.downsize = function (number, scale) {
// if there is no scaling, simply return the number
if (scale === 0) {
return Number(number)
} else {
// if the scale is the same as the precision, account for this edge case.
- var decimals = (scale === precision) ? -1 : scale - precision
- return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals))
+ var adjustedNumber = number
+ while (adjustedNumber.length < scale) {
+ adjustedNumber = '0' + adjustedNumber
+ }
+ return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale))
}
}
diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js
index 6f8c19a3c..c3350fcc1 100644
--- a/ui/app/components/pending-tx.js
+++ b/ui/app/components/pending-tx.js
@@ -33,7 +33,7 @@ function PendingTx () {
PendingTx.prototype.render = function () {
const props = this.props
- const { currentCurrency, blockGasLimit, computedBalances } = props
+ const { currentCurrency, blockGasLimit } = props
const conversionRate = props.conversionRate
const txMeta = this.gatherTxMeta()
@@ -42,8 +42,8 @@ PendingTx.prototype.render = function () {
// Account Details
const address = txParams.from || props.selectedAddress
const identity = props.identities[address] || { address: address }
- const account = computedBalances[address]
- const balance = account ? account.ethBalance : '0x0'
+ const account = props.accounts[address]
+ const balance = account ? account.balance : '0x0'
// recipient check
const isValidAddress = !txParams.to || util.isValidAddress(txParams.to)
diff --git a/ui/app/components/pending-typed-msg-details.js b/ui/app/components/pending-typed-msg-details.js
new file mode 100644
index 000000000..b5fd29f71
--- /dev/null
+++ b/ui/app/components/pending-typed-msg-details.js
@@ -0,0 +1,59 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+
+const AccountPanel = require('./account-panel')
+const TypedMessageRenderer = require('./typed-message-renderer')
+
+module.exports = PendingMsgDetails
+
+inherits(PendingMsgDetails, Component)
+function PendingMsgDetails () {
+ Component.call(this)
+}
+
+PendingMsgDetails.prototype.render = function () {
+ var state = this.props
+ var msgData = state.txData
+
+ var msgParams = msgData.msgParams || {}
+ var address = msgParams.from || state.selectedAddress
+ var identity = state.identities[address] || { address: address }
+ var account = state.accounts[address] || { address: address }
+
+ var { data } = msgParams
+
+ return (
+ h('div', {
+ key: msgData.id,
+ style: {
+ margin: '10px 20px',
+ },
+ }, [
+
+ // account that will sign
+ h(AccountPanel, {
+ showFullAddress: true,
+ identity: identity,
+ account: account,
+ imageifyIdenticons: state.imageifyIdenticons,
+ }),
+
+ // message data
+ h('div', {
+ style: {
+ height: '260px',
+ },
+ }, [
+ h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'),
+ h(TypedMessageRenderer, {
+ value: data,
+ style: {
+ height: '215px',
+ },
+ }),
+ ]),
+
+ ])
+ )
+}
diff --git a/ui/app/components/pending-typed-msg.js b/ui/app/components/pending-typed-msg.js
new file mode 100644
index 000000000..f8926d0a3
--- /dev/null
+++ b/ui/app/components/pending-typed-msg.js
@@ -0,0 +1,46 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const PendingTxDetails = require('./pending-typed-msg-details')
+
+module.exports = PendingMsg
+
+inherits(PendingMsg, Component)
+function PendingMsg () {
+ Component.call(this)
+}
+
+PendingMsg.prototype.render = function () {
+ var state = this.props
+ var msgData = state.txData
+
+ return (
+
+ h('div', {
+ key: msgData.id,
+ }, [
+
+ // header
+ h('h3', {
+ style: {
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ }, 'Sign Message'),
+
+ // message details
+ h(PendingTxDetails, state),
+
+ // sign + cancel
+ h('.flex-row.flex-space-around', [
+ h('button', {
+ onClick: state.cancelTypedMessage,
+ }, 'Cancel'),
+ h('button', {
+ onClick: state.signTypedMessage,
+ }, 'Sign'),
+ ]),
+ ])
+
+ )
+}
diff --git a/ui/app/components/typed-message-renderer.js b/ui/app/components/typed-message-renderer.js
new file mode 100644
index 000000000..a042b57be
--- /dev/null
+++ b/ui/app/components/typed-message-renderer.js
@@ -0,0 +1,42 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const inherits = require('util').inherits
+const extend = require('xtend')
+
+module.exports = TypedMessageRenderer
+
+inherits(TypedMessageRenderer, Component)
+function TypedMessageRenderer () {
+ Component.call(this)
+}
+
+TypedMessageRenderer.prototype.render = function () {
+ const props = this.props
+ const { value, style } = props
+ const text = renderTypedData(value)
+
+ const defaultStyle = extend({
+ width: '315px',
+ maxHeight: '210px',
+ resize: 'none',
+ border: 'none',
+ background: 'white',
+ padding: '3px',
+ overflow: 'scroll',
+ }, style)
+
+ return (
+ h('div.font-small', {
+ style: defaultStyle,
+ }, text)
+ )
+}
+
+function renderTypedData(values) {
+ return values.map(function (value) {
+ return h('div', {}, [
+ h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'),
+ h('div', {}, value.value),
+ ])
+ })
+} \ No newline at end of file
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 15fb9a59f..cb1afedfe 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -10,6 +10,7 @@ const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notific
const PendingTx = require('./components/pending-tx')
const PendingMsg = require('./components/pending-msg')
const PendingPersonalMsg = require('./components/pending-personal-msg')
+const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading')
module.exports = connect(mapStateToProps)(ConfirmTxScreen)
@@ -22,6 +23,7 @@ function mapStateToProps (state) {
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
+ unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
index: state.appState.currentView.context,
warning: state.appState.warning,
network: state.metamask.network,
@@ -41,9 +43,9 @@ function ConfirmTxScreen () {
ConfirmTxScreen.prototype.render = function () {
const props = this.props
const { network, provider, unapprovedTxs, currentCurrency, computedBalances,
- unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props
+ unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, conversionRate, blockGasLimit } = props
- var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network)
+ var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
var txData = unconfTxList[props.index] || {}
var txParams = txData.params || {}
@@ -112,8 +114,10 @@ ConfirmTxScreen.prototype.render = function () {
cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList),
signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData),
+ signTypedMessage: this.signTypedMessage.bind(this, txData),
cancelMessage: this.cancelMessage.bind(this, txData),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
}),
])
)
@@ -136,6 +140,9 @@ function currentTxView (opts) {
} else if (type === 'personal_sign') {
log.debug('rendering personal_sign message')
return h(PendingPersonalMsg, opts)
+ } else if (type === 'eth_signTypedData') {
+ log.debug('rendering eth_signTypedData message')
+ return h(PendingTypedMsg, opts)
}
}
}
@@ -184,6 +191,14 @@ ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) {
this.props.dispatch(actions.signPersonalMsg(params))
}
+ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing typed message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ this.props.dispatch(actions.signTypedMsg(params))
+}
+
ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
log.info('canceling message')
this.stopPropagation(event)
@@ -196,6 +211,12 @@ ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) {
this.props.dispatch(actions.cancelPersonalMsg(msgData))
}
+ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
+ log.info('canceling typed message')
+ this.stopPropagation(event)
+ this.props.dispatch(actions.cancelTypedMsg(msgData))
+}
+
ConfirmTxScreen.prototype.goHome = function (event) {
this.stopPropagation(event)
this.props.dispatch(actions.goHome())
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index 3a98d53a9..349c25b96 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -574,9 +574,9 @@ function checkUnconfActions (state) {
function getUnconfActionList (state) {
const { unapprovedTxs, unapprovedMsgs,
- unapprovedPersonalMsgs, network } = state.metamask
+ unapprovedPersonalMsgs, unapprovedTypedMessages, network } = state.metamask
- const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network)
+ const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
return unconfActionList
}
diff --git a/ui/index.js b/ui/index.js
index a729138d3..ae05cbe67 100644
--- a/ui/index.js
+++ b/ui/index.js
@@ -37,7 +37,7 @@ function startApp (metamaskState, accountManager, opts) {
})
// if unconfirmed txs, start on txConf page
- const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network)
+ const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
if (unapprovedTxsAll.length > 0) {
store.dispatch(actions.showConfTxPage())
}
diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js
index 5def23e51..341567e2f 100644
--- a/ui/lib/tx-helper.js
+++ b/ui/lib/tx-helper.js
@@ -1,20 +1,27 @@
const valuesFor = require('../app/util').valuesFor
-module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) {
+module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network) {
log.debug('tx-helper called with params:')
- log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network })
+ log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network })
const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs)
log.debug(`tx helper found ${txValues.length} unapproved txs`)
+
const msgValues = valuesFor(unapprovedMsgs)
log.debug(`tx helper found ${msgValues.length} unsigned messages`)
let allValues = txValues.concat(msgValues)
+
const personalValues = valuesFor(personalMsgs)
log.debug(`tx helper found ${personalValues.length} unsigned personal messages`)
allValues = allValues.concat(personalValues)
+
+ const typedValues = valuesFor(typedMessages)
+ log.debug(`tx helper found ${typedValues.length} unsigned typed messages`)
+ allValues = allValues.concat(typedValues)
+
allValues = allValues.sort((a, b) => {
return a.time > b.time
})
return allValues
-}
+} \ No newline at end of file