aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/scripts')
-rw-r--r--app/scripts/background.js3
-rw-r--r--app/scripts/contentscript.js7
-rw-r--r--app/scripts/controllers/balance.js10
-rw-r--r--app/scripts/controllers/computed-balances.js29
-rw-r--r--app/scripts/controllers/network.js6
-rw-r--r--app/scripts/controllers/transactions.js3
-rw-r--r--app/scripts/inpage.js10
-rw-r--r--app/scripts/lib/account-tracker.js23
-rw-r--r--app/scripts/lib/createLoggerMiddleware.js4
-rw-r--r--app/scripts/lib/createOriginMiddleware.js4
-rw-r--r--app/scripts/lib/createProviderMiddleware.js5
-rw-r--r--app/scripts/lib/events-proxy.js4
-rw-r--r--app/scripts/lib/nodeify.js14
-rw-r--r--app/scripts/lib/pending-balance-calculator.js2
-rw-r--r--app/scripts/lib/pending-tx-tracker.js41
-rw-r--r--app/scripts/lib/port-stream.js2
-rw-r--r--app/scripts/lib/tx-gas-utils.js2
-rw-r--r--app/scripts/lib/tx-state-history-helper.js10
-rw-r--r--app/scripts/lib/tx-state-manager.js10
-rw-r--r--app/scripts/lib/typed-message-manager.js123
-rw-r--r--app/scripts/metamask-controller.js116
-rw-r--r--app/scripts/platforms/extension.js9
22 files changed, 367 insertions, 70 deletions
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/contentscript.js b/app/scripts/contentscript.js
index b4708189e..ffbbc73cc 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -7,7 +7,9 @@ const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
const PortStream = require('./lib/port-stream.js')
-const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
+const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString()
+const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n'
+const inpageBundle = inpageContent + inpageSuffix
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
@@ -25,8 +27,7 @@ function setupInjection () {
try {
// inject in-page script
var scriptTag = document.createElement('script')
- scriptTag.src = extension.extension.getURL('scripts/inpage.js')
- scriptTag.textContent = inpageText
+ scriptTag.textContent = inpageBundle
scriptTag.onload = function () { this.parentNode.removeChild(this) }
var container = document.head || document.documentElement
// append as first child
diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js
index 4fa4c78fe..f83f294cc 100644
--- a/app/scripts/controllers/balance.js
+++ b/app/scripts/controllers/balance.js
@@ -5,7 +5,9 @@ const BN = require('ethereumjs-util').BN
class BalanceController {
constructor (opts = {}) {
+ this._validateParams(opts)
const { address, accountTracker, txController, blockTracker } = opts
+
this.address = address
this.accountTracker = accountTracker
this.txController = txController
@@ -65,6 +67,14 @@ class BalanceController {
return pending
}
+ _validateParams (opts) {
+ const { address, accountTracker, txController, blockTracker } = opts
+ if (!address || !accountTracker || !txController || !blockTracker) {
+ const error = 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.'
+ throw new Error(error)
+ }
+ }
+
}
module.exports = BalanceController
diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js
index 2479e1b3a..907b087cf 100644
--- a/app/scripts/controllers/computed-balances.js
+++ b/app/scripts/controllers/computed-balances.js
@@ -20,23 +20,34 @@ class ComputedbalancesController {
}
updateAllBalances () {
- for (let address in this.accountTracker.store.getState().accounts) {
+ Object.keys(this.balances).forEach((balance) => {
+ const address = balance.address
this.balances[address].updateBalance()
- }
+ })
}
_initBalanceUpdating () {
const store = this.accountTracker.store.getState()
- this.addAnyAccountsFromStore(store)
- this.accountTracker.store.subscribe(this.addAnyAccountsFromStore.bind(this))
+ this.syncAllAccountsFromStore(store)
+ this.accountTracker.store.subscribe(this.syncAllAccountsFromStore.bind(this))
}
- addAnyAccountsFromStore(store) {
- const balances = store.accounts
+ syncAllAccountsFromStore (store) {
+ const upstream = Object.keys(store.accounts)
+ const balances = Object.keys(this.balances)
+ .map(address => this.balances[address])
- for (let address in balances) {
+ // Follow new addresses
+ for (const address in balances) {
this.trackAddressIfNotAlready(address)
}
+
+ // Unfollow old ones
+ balances.forEach(({ address }) => {
+ if (!upstream.includes(address)) {
+ delete this.balances[address]
+ }
+ })
}
trackAddressIfNotAlready (address) {
@@ -47,14 +58,14 @@ class ComputedbalancesController {
}
trackAddress (address) {
- let updater = new BalanceController({
+ const updater = new BalanceController({
address,
accountTracker: this.accountTracker,
txController: this.txController,
blockTracker: this.blockTracker,
})
updater.store.subscribe((accountBalance) => {
- let newState = this.store.getState()
+ const newState = this.store.getState()
newState.computedBalances[address] = accountBalance
this.store.updateState(newState)
})
diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js
index 0f9db4d53..23afdc12b 100644
--- a/app/scripts/controllers/network.js
+++ b/app/scripts/controllers/network.js
@@ -51,6 +51,10 @@ module.exports = class NetworkController extends EventEmitter {
}
lookupNetwork () {
+ // Prevent firing when provider is not defined.
+ if (!this.ethQuery || !this.ethQuery.sendAsync) {
+ return
+ }
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network)
@@ -101,7 +105,7 @@ module.exports = class NetworkController extends EventEmitter {
this.emit('networkDidChange')
}
- _configureStandardProvider(_providerParams) {
+ _configureStandardProvider (_providerParams) {
const providerParams = extend(this._baseProviderParams, _providerParams)
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index 481dd62a5..d5fde033b 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -59,9 +59,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/inpage.js b/app/scripts/inpage.js
index 9e98c044b..b6889b00f 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -1,6 +1,7 @@
/*global Web3*/
cleanContextForImports()
require('web3/dist/web3.min.js')
+const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream')
// const PingStream = require('ping-pong-stream/ping')
// const endOfStream = require('end-of-stream')
@@ -8,6 +9,10 @@ const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
+const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
+window.log = log
+log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
+
//
// setup plugin communication
@@ -28,9 +33,9 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream)
var web3 = new Web3(inpageProvider)
web3.setProvider = function () {
- console.log('MetaMask - overrode web3.setProvider')
+ log.debug('MetaMask - overrode web3.setProvider')
}
-console.log('MetaMask - injected web3')
+log.debug('MetaMask - injected web3')
// export global web3, with usage-detection
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
@@ -65,4 +70,3 @@ function restoreContextAfterImports () {
console.warn('MetaMask - global.define could not be overwritten.')
}
}
-
diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js
index cdc21282d..ce6642150 100644
--- a/app/scripts/lib/account-tracker.js
+++ b/app/scripts/lib/account-tracker.js
@@ -38,6 +38,29 @@ class AccountTracker extends EventEmitter {
// public
//
+ syncWithAddresses (addresses) {
+ const accounts = this.store.getState().accounts
+ const locals = Object.keys(accounts)
+
+ const toAdd = []
+ addresses.forEach((upstream) => {
+ if (!locals.includes(upstream)) {
+ toAdd.push(upstream)
+ }
+ })
+
+ const toRemove = []
+ locals.forEach((local) => {
+ if (!addresses.includes(local)) {
+ toRemove.push(local)
+ }
+ })
+
+ toAdd.forEach(upstream => this.addAccount(upstream))
+ toRemove.forEach(local => this.removeAccount(local))
+ this._updateAccounts()
+ }
+
addAccount (address) {
const accounts = this.store.getState().accounts
accounts[address] = {}
diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js
index b92a965de..2707cbd9e 100644
--- a/app/scripts/lib/createLoggerMiddleware.js
+++ b/app/scripts/lib/createLoggerMiddleware.js
@@ -1,7 +1,7 @@
// log rpc activity
module.exports = createLoggerMiddleware
-function createLoggerMiddleware({ origin }) {
+function createLoggerMiddleware ({ origin }) {
return function loggerMiddleware (req, res, next, end) {
next((cb) => {
if (res.error) {
@@ -12,4 +12,4 @@ function createLoggerMiddleware({ origin }) {
cb()
})
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js
index e1e097cc4..f8bdb2dc2 100644
--- a/app/scripts/lib/createOriginMiddleware.js
+++ b/app/scripts/lib/createOriginMiddleware.js
@@ -1,9 +1,9 @@
// append dapp origin domain to request
module.exports = createOriginMiddleware
-function createOriginMiddleware({ origin }) {
+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
index 6dd192411..4e667bac2 100644
--- a/app/scripts/lib/createProviderMiddleware.js
+++ b/app/scripts/lib/createProviderMiddleware.js
@@ -1,8 +1,7 @@
-
module.exports = createProviderMiddleware
// forward requests to provider
-function createProviderMiddleware({ provider }) {
+function createProviderMiddleware ({ provider }) {
return (req, res, next, end) => {
provider.sendAsync(req, (err, _res) => {
if (err) return end(err)
@@ -10,4 +9,4 @@ function createProviderMiddleware({ provider }) {
end()
})
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js
index d1199a278..c0a490b05 100644
--- a/app/scripts/lib/events-proxy.js
+++ b/app/scripts/lib/events-proxy.js
@@ -1,4 +1,4 @@
-module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
+module.exports = function createEventEmitterProxy (eventEmitter, listeners) {
let target = eventEmitter
const eventHandlers = listeners || {}
const proxy = new Proxy({}, {
@@ -28,4 +28,4 @@ module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
}
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..9b595d93c 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(){
+ 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/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js
index cea642f1a..6ae526463 100644
--- a/app/scripts/lib/pending-balance-calculator.js
+++ b/app/scripts/lib/pending-balance-calculator.js
@@ -13,7 +13,7 @@ class PendingBalanceCalculator {
this.getNetworkBalance = getBalance
}
- async getBalance() {
+ async getBalance () {
const results = await Promise.all([
this.getNetworkBalance(),
this.getPendingTransactions(),
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index 6f1601586..0d7c6a92c 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
@@ -78,14 +81,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
const errorMessage = err.message.toLowerCase()
const isKnownTx = (
// geth
- errorMessage.includes('replacement transaction underpriced')
- || errorMessage.includes('known transaction')
+ errorMessage.includes('replacement transaction underpriced') ||
+ errorMessage.includes('known transaction') ||
// parity
- || errorMessage.includes('gas price too low to replace')
- || errorMessage.includes('transaction with the same hash was already imported')
+ errorMessage.includes('gas price too low to replace') ||
+ errorMessage.includes('transaction with the same hash was already imported') ||
// other
- || errorMessage.includes('gateway timeout')
- || errorMessage.includes('nonce too low')
+ errorMessage.includes('gateway timeout') ||
+ errorMessage.includes('nonce too low')
)
// ignore resubmit warnings, return early
if (isKnownTx) return
@@ -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/port-stream.js b/app/scripts/lib/port-stream.js
index 648d88087..a9716fb00 100644
--- a/app/scripts/lib/port-stream.js
+++ b/app/scripts/lib/port-stream.js
@@ -1,6 +1,6 @@
const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits
-const noop = function(){}
+const noop = function () {}
module.exports = PortDuplexStream
diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/lib/tx-gas-utils.js
index 41f67e230..7e72ea71d 100644
--- a/app/scripts/lib/tx-gas-utils.js
+++ b/app/scripts/lib/tx-gas-utils.js
@@ -81,4 +81,4 @@ module.exports = class txProvideUtil {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
}
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js
index db6e3bc9f..94c7b6792 100644
--- a/app/scripts/lib/tx-state-history-helper.js
+++ b/app/scripts/lib/tx-state-history-helper.js
@@ -9,7 +9,7 @@ module.exports = {
}
-function migrateFromSnapshotsToDiffs(longHistory) {
+function migrateFromSnapshotsToDiffs (longHistory) {
return (
longHistory
// convert non-initial history entries into diffs
@@ -20,22 +20,22 @@ function migrateFromSnapshotsToDiffs(longHistory) {
)
}
-function generateHistoryEntry(previousState, newState, note) {
+function generateHistoryEntry (previousState, newState, note) {
const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry
if (note && entry[0]) entry[0].note = note
return entry
}
-function replayHistory(_shortHistory) {
+function replayHistory (_shortHistory) {
const shortHistory = clone(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
}
-function snapshotFromTxMeta(txMeta) {
+function snapshotFromTxMeta (txMeta) {
// create txMeta snapshot for history
const snapshot = clone(txMeta)
// dont include previous history in this snapshot
delete snapshot.history
return snapshot
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js
index cf8117864..0fd6bed4b 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`)
@@ -85,7 +91,7 @@ module.exports = class TransactionStateManger extends EventEmitter {
updateTx (txMeta, note) {
if (txMeta.txParams) {
Object.keys(txMeta.txParams).forEach((key) => {
- let value = txMeta.txParams[key]
+ const value = txMeta.txParams[key]
if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`)
if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed')
})
@@ -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..ad42a39fb 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1,6 +1,5 @@
const EventEmitter = require('events')
const extend = require('xtend')
-const promiseToCallback = require('promise-to-callback')
const pump = require('pump')
const Dnode = require('dnode')
const ObservableStore = require('obs-store')
@@ -25,6 +24,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')
@@ -95,25 +95,20 @@ module.exports = class MetamaskController extends EventEmitter {
// key mgmt
this.keyringController = new KeyringController({
initState: initState.KeyringController,
- accountTracker: this.accountTracker,
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
encryptor: opts.encryptor || undefined,
})
// If only one account exists, make sure it is selected.
- this.keyringController.store.subscribe((state) => {
- const addresses = Object.keys(state.walletNicknames || {})
+ this.keyringController.memStore.subscribe((state) => {
+ const addresses = state.keyrings.reduce((res, keyring) => {
+ return res.concat(keyring.accounts)
+ }, [])
if (addresses.length === 1) {
const address = addresses[0]
this.preferencesController.setSelectedAddress(address)
}
- })
- this.keyringController.on('newAccount', (address) => {
- this.preferencesController.setSelectedAddress(address)
- this.accountTracker.addAccount(address)
- })
- this.keyringController.on('removedAccount', (address) => {
- this.accountTracker.removeAccount(address)
+ this.accountTracker.syncWithAddresses(addresses)
})
// address book controller
@@ -161,6 +156,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 +198,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))
@@ -239,6 +236,7 @@ module.exports = class MetamaskController extends EventEmitter {
processMessage: this.newUnsignedMessage.bind(this),
// personal_sign msg signing
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
+ processTypedMessage: this.newUnsignedTypedMessage.bind(this),
}
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
@@ -283,6 +281,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(),
@@ -324,13 +323,13 @@ module.exports = class MetamaskController extends EventEmitter {
createShapeShiftTx: this.createShapeShiftTx.bind(this),
// primary HD keyring management
- addNewAccount: this.addNewAccount.bind(this),
+ addNewAccount: nodeify(this.addNewAccount, this),
placeSeedWords: this.placeSeedWords.bind(this),
clearSeedWordCache: this.clearSeedWordCache.bind(this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
// vault management
- submitPassword: this.submitPassword.bind(this),
+ submitPassword: nodeify(keyringController.submitPassword, keyringController),
// network management
setProviderType: nodeify(networkController.setProviderType, networkController),
@@ -346,8 +345,8 @@ module.exports = class MetamaskController extends EventEmitter {
// KeyringController
setLocked: nodeify(keyringController.setLocked, keyringController),
- createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain, keyringController),
- createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore, keyringController),
+ createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this),
+ createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this),
addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController),
saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController),
exportAccount: nodeify(keyringController.exportAccount, keyringController),
@@ -364,6 +363,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),
@@ -464,20 +467,43 @@ module.exports = class MetamaskController extends EventEmitter {
// Vault Management
//
- submitPassword (password, cb) {
- return this.keyringController.submitPassword(password)
- .then((newState) => { cb(null, newState) })
- .catch((reason) => { cb(reason) })
+ async createNewVaultAndKeychain (password, cb) {
+ const vault = await this.keyringController.createNewVaultAndKeychain(password)
+ this.selectFirstIdentity(vault)
+ return vault
+ }
+
+ async createNewVaultAndRestore (password, seed, cb) {
+ const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
+ this.selectFirstIdentity(vault)
+ return vault
+ }
+
+ selectFirstIdentity (vault) {
+ const { identities } = vault
+ const address = Object.keys(identities)[0]
+ this.preferencesController.setSelectedAddress(address)
}
//
// Opinionated Keyring Management
//
- addNewAccount (cb) {
+ async addNewAccount (cb) {
const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0]
if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found'))
- promiseToCallback(this.keyringController.addNewAccount(primaryKeyring))(cb)
+ const keyringController = this.keyringController
+ const oldAccounts = await keyringController.getAccounts()
+ const keyState = await keyringController.addNewAccount(primaryKeyring)
+ const newAccounts = await keyringController.getAccounts()
+
+ newAccounts.forEach((address) => {
+ if (!oldAccounts.includes(address)) {
+ this.preferencesController.setSelectedAddress(address)
+ }
+ })
+
+ return keyState
}
// Adds the current vault's seed words to the UI's state tree.
@@ -556,6 +582,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 +666,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 +692,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/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index 0afe04b74..2f47512eb 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -17,6 +17,15 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version
}
+ getPlatformInfo (cb) {
+ try {
+ extension.runtime.getPlatformInfo((platform) => {
+ cb(null, platform)
+ })
+ } catch (e) {
+ cb(e)
+ }
+ }
}
module.exports = ExtensionPlatform