aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/images/icon-32.pngbin0 -> 1730 bytes
-rw-r--r--app/images/icon-64.pngbin0 -> 3573 bytes
-rw-r--r--app/manifest.json11
-rw-r--r--app/notification.html16
-rw-r--r--app/scripts/account-import-strategies/index.js45
-rw-r--r--app/scripts/background.js281
-rw-r--r--app/scripts/chromereload.js6
-rw-r--r--app/scripts/config.js7
-rw-r--r--app/scripts/contentscript.js52
-rw-r--r--app/scripts/first-time-state.js11
-rw-r--r--app/scripts/inpage.js25
-rw-r--r--app/scripts/keyring-controller.js554
-rw-r--r--app/scripts/keyrings/hd.js125
-rw-r--r--app/scripts/keyrings/simple.js100
-rw-r--r--app/scripts/lib/auto-faucet.js5
-rw-r--r--app/scripts/lib/auto-reload.js21
-rw-r--r--app/scripts/lib/config-manager.js275
-rw-r--r--app/scripts/lib/controllers/currency.js70
-rw-r--r--app/scripts/lib/controllers/preferences.js33
-rw-r--r--app/scripts/lib/controllers/shapeshift.js104
-rw-r--r--app/scripts/lib/eth-store.js132
-rw-r--r--app/scripts/lib/extension-instance.js19
-rw-r--r--app/scripts/lib/id-management.js19
-rw-r--r--app/scripts/lib/idStore-migrator.js80
-rw-r--r--app/scripts/lib/idStore.js364
-rw-r--r--app/scripts/lib/inpage-provider.js130
-rw-r--r--app/scripts/lib/is-popup-or-notification.js8
-rw-r--r--app/scripts/lib/message-manager.js151
-rw-r--r--app/scripts/lib/migrations.js5
-rw-r--r--app/scripts/lib/migrator/index.js51
-rw-r--r--app/scripts/lib/nodeify.js24
-rw-r--r--app/scripts/lib/notifications.js174
-rw-r--r--app/scripts/lib/obj-multiplex.js8
-rw-r--r--app/scripts/lib/port-stream.js10
-rw-r--r--app/scripts/lib/random-id.js9
-rw-r--r--app/scripts/lib/remote-store.js97
-rw-r--r--app/scripts/lib/sig-util.js28
-rw-r--r--app/scripts/lib/stream-utils.js9
-rw-r--r--app/scripts/lib/tx-utils.js132
-rw-r--r--app/scripts/metamask-controller.js729
-rw-r--r--app/scripts/migrations/002.js19
-rw-r--r--app/scripts/migrations/003.js19
-rw-r--r--app/scripts/migrations/004.js20
-rw-r--r--app/scripts/migrations/005.js44
-rw-r--r--app/scripts/migrations/006.js43
-rw-r--r--app/scripts/migrations/007.js40
-rw-r--r--app/scripts/migrations/008.js38
-rw-r--r--app/scripts/migrations/009.js43
-rw-r--r--app/scripts/migrations/010.js38
-rw-r--r--app/scripts/migrations/011.js33
-rw-r--r--app/scripts/migrations/_multi-keyring.js51
-rw-r--r--app/scripts/migrations/index.js25
-rw-r--r--app/scripts/notice-controller.js94
-rw-r--r--app/scripts/popup-core.js63
-rw-r--r--app/scripts/popup.js96
-rw-r--r--app/scripts/transaction-manager.js390
56 files changed, 3625 insertions, 1351 deletions
diff --git a/app/images/icon-32.png b/app/images/icon-32.png
new file mode 100644
index 000000000..f801ebb6b
--- /dev/null
+++ b/app/images/icon-32.png
Binary files differ
diff --git a/app/images/icon-64.png b/app/images/icon-64.png
new file mode 100644
index 000000000..b3019ad65
--- /dev/null
+++ b/app/images/icon-64.png
Binary files differ
diff --git a/app/manifest.json b/app/manifest.json
index e5e08c4b6..95475ddf1 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,15 +1,16 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "2.9.2",
+ "version": "3.2.2",
"manifest_version": 2,
+ "author": "https://metamask.io",
"description": "Ethereum Browser Extension",
"commands": {
"_execute_browser_action": {
"suggested_key": {
"windows": "Alt+Shift+M",
"mac": "Alt+Shift+M",
- "chromeos": "Search+M",
+ "chromeos": "Alt+Shift+M",
"linux": "Alt+Shift+M"
}
}
@@ -28,7 +29,8 @@
"scripts": [
"scripts/chromereload.js",
"scripts/background.js"
- ]
+ ],
+ "persistent": true
},
"browser_action": {
"default_icon": {
@@ -53,9 +55,8 @@
}
],
"permissions": [
- "notifications",
"storage",
- "tabs",
+ "clipboardWrite",
"http://localhost:8545/"
],
"web_accessible_resources": [
diff --git a/app/notification.html b/app/notification.html
new file mode 100644
index 000000000..cc485da7f
--- /dev/null
+++ b/app/notification.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>MetaMask Notification</title>
+ <style>
+ body {
+ overflow: hidden;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="app-content"></div>
+ <script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
+ </body>
+</html>
diff --git a/app/scripts/account-import-strategies/index.js b/app/scripts/account-import-strategies/index.js
new file mode 100644
index 000000000..d5124eb7f
--- /dev/null
+++ b/app/scripts/account-import-strategies/index.js
@@ -0,0 +1,45 @@
+const Wallet = require('ethereumjs-wallet')
+const importers = require('ethereumjs-wallet/thirdparty')
+const ethUtil = require('ethereumjs-util')
+
+const accountImporter = {
+
+ importAccount(strategy, args) {
+ try {
+ const importer = this.strategies[strategy]
+ const privateKeyHex = importer.apply(null, args)
+ return Promise.resolve(privateKeyHex)
+ } catch (e) {
+ return Promise.reject(e)
+ }
+ },
+
+ strategies: {
+ 'Private Key': (privateKey) => {
+ const stripped = ethUtil.stripHexPrefix(privateKey)
+ return stripped
+ },
+ 'JSON File': (input, password) => {
+ let wallet
+ try {
+ wallet = importers.fromEtherWallet(input, password)
+ } catch (e) {
+ console.log('Attempt to import as EtherWallet format failed, trying V3...')
+ }
+
+ if (!wallet) {
+ wallet = Wallet.fromV3(input, password, true)
+ }
+
+ return walletToPrivateKey(wallet)
+ },
+ },
+
+}
+
+function walletToPrivateKey (wallet) {
+ const privateKeyBuffer = wallet.getPrivateKey()
+ return ethUtil.bufferToHex(privateKeyBuffer)
+}
+
+module.exports = accountImporter
diff --git a/app/scripts/background.js b/app/scripts/background.js
index e04309e74..2e5a992b9 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -1,192 +1,147 @@
const urlUtil = require('url')
-const extend = require('xtend')
-const Dnode = require('dnode')
-const eos = require('end-of-stream')
+const endOfStream = require('end-of-stream')
+const asyncQ = require('async-q')
+const pipe = require('pump')
+const LocalStorageStore = require('obs-store/lib/localStorage')
+const storeTransform = require('obs-store/lib/transform')
+const Migrator = require('./lib/migrator/')
+const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js')
-const createUnlockRequestNotification = require('./lib/notifications.js').createUnlockRequestNotification
-const createTxNotification = require('./lib/notifications.js').createTxNotification
-const createMsgNotification = require('./lib/notifications.js').createMsgNotification
-const messageManager = require('./lib/message-manager')
-const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
+const notification = require('./lib/notifications.js')
const MetamaskController = require('./metamask-controller')
const extension = require('./lib/extension')
+const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config'
+const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
+let popupIsOpen = false
-const controller = new MetamaskController({
- // User confirmation callbacks:
- showUnconfirmedMessage,
- unlockAccountMessage,
- showUnconfirmedTx,
- // Persistence Methods:
- setData,
- loadData,
-})
-const idStore = controller.idStore
+// state persistence
+const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
-function unlockAccountMessage () {
- createUnlockRequestNotification({
- title: 'Account Unlock Request',
- })
-}
+// initialization flow
+asyncQ.waterfall([
+ () => loadStateFromPersistence(),
+ (initState) => setupController(initState),
+])
+.then(() => console.log('MetaMask initialization complete.'))
+.catch((err) => { console.error(err) })
-function showUnconfirmedMessage (msgParams, msgId) {
- var controllerState = controller.getState()
+//
+// State and Persistence
+//
- createMsgNotification({
- imageifyIdenticons: false,
- txData: {
- msgParams: msgParams,
- time: (new Date()).getTime(),
+function loadStateFromPersistence() {
+ // migrations
+ let migrator = new Migrator({ migrations })
+ let initialState = migrator.generateInitialState(firstTimeState)
+ return asyncQ.waterfall([
+ // read from disk
+ () => Promise.resolve(diskStore.getState() || initialState),
+ // migrate data
+ (versionedData) => migrator.migrateData(versionedData),
+ // write to disk
+ (versionedData) => {
+ diskStore.putState(versionedData)
+ return Promise.resolve(versionedData)
},
- identities: controllerState.identities,
- accounts: controllerState.accounts,
- onConfirm: idStore.approveMessage.bind(idStore, msgId, noop),
- onCancel: idStore.cancelMessage.bind(idStore, msgId),
- })
+ // resolve to just data
+ (versionedData) => Promise.resolve(versionedData.data),
+ ])
}
-function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
- var controllerState = controller.getState()
+function setupController (initState) {
- createTxNotification({
- imageifyIdenticons: false,
- txData: {
- txParams: txParams,
- time: (new Date()).getTime(),
- },
- identities: controllerState.identities,
- accounts: controllerState.accounts,
- onConfirm: idStore.approveTransaction.bind(idStore, txData.id, noop),
- onCancel: idStore.cancelTransaction.bind(idStore, txData.id),
- })
-}
+ //
+ // MetaMask Controller
+ //
-//
-// connect to other contexts
-//
-
-extension.runtime.onConnect.addListener(connectRemote)
-function connectRemote (remotePort) {
- var isMetaMaskInternalProcess = (remotePort.name === 'popup')
- var portStream = new PortStream(remotePort)
- if (isMetaMaskInternalProcess) {
- // communication with popup
- setupTrustedCommunication(portStream, 'MetaMask')
- } else {
- // communication with page
- var originDomain = urlUtil.parse(remotePort.sender.url).hostname
- setupUntrustedCommunication(portStream, originDomain)
+ const controller = new MetamaskController({
+ // User confirmation callbacks:
+ showUnconfirmedMessage: triggerUi,
+ unlockAccountMessage: triggerUi,
+ showUnapprovedTx: triggerUi,
+ // initial state
+ initState,
+ })
+ global.metamaskController = controller
+
+ // setup state persistence
+ pipe(
+ controller.store,
+ storeTransform(versionifyData),
+ diskStore
+ )
+
+ function versionifyData(state) {
+ let versionedData = diskStore.getState()
+ versionedData.data = state
+ return versionedData
}
-}
-function setupUntrustedCommunication (connectionStream, originDomain) {
- // setup multiplexing
- var mx = setupMultiplex(connectionStream)
- // connect features
- controller.setupProviderConnection(mx.createStream('provider'), originDomain)
- controller.setupPublicConfig(mx.createStream('publicConfig'))
-}
+ //
+ // connect to other contexts
+ //
+
+ extension.runtime.onConnect.addListener(connectRemote)
+ function connectRemote (remotePort) {
+ var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification'
+ var portStream = new PortStream(remotePort)
+ if (isMetaMaskInternalProcess) {
+ // communication with popup
+ popupIsOpen = popupIsOpen || (remotePort.name === 'popup')
+ controller.setupTrustedCommunication(portStream, 'MetaMask', remotePort.name)
+ // record popup as closed
+ if (remotePort.name === 'popup') {
+ endOfStream(portStream, () => {
+ popupIsOpen = false
+ })
+ }
+ } else {
+ // communication with page
+ var originDomain = urlUtil.parse(remotePort.sender.url).hostname
+ controller.setupUntrustedCommunication(portStream, originDomain)
+ }
+ }
-function setupTrustedCommunication (connectionStream, originDomain) {
- // setup multiplexing
- var mx = setupMultiplex(connectionStream)
- // connect features
- setupControllerConnection(mx.createStream('controller'))
- controller.setupProviderConnection(mx.createStream('provider'), originDomain)
-}
+ //
+ // User Interface setup
+ //
+
+ updateBadge()
+ controller.txManager.on('updateBadge', updateBadge)
+ controller.messageManager.on('updateBadge', updateBadge)
+
+ // plugin badge text
+ function updateBadge () {
+ var label = ''
+ var unapprovedTxCount = controller.txManager.unapprovedTxCount
+ var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
+ var count = unapprovedTxCount + unapprovedMsgCount
+ if (count) {
+ label = String(count)
+ }
+ extension.browserAction.setBadgeText({ text: label })
+ extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
+ }
-//
-// remote features
-//
+ return Promise.resolve()
-function setupControllerConnection (stream) {
- controller.stream = stream
- var api = controller.getApi()
- var dnode = Dnode(api)
- stream.pipe(dnode).pipe(stream)
- dnode.on('remote', (remote) => {
- // push updates to popup
- controller.ethStore.on('update', controller.sendUpdate.bind(controller))
- controller.remote = remote
- idStore.on('update', controller.sendUpdate.bind(controller))
-
- // teardown on disconnect
- eos(stream, () => {
- controller.ethStore.removeListener('update', controller.sendUpdate.bind(controller))
- })
- })
}
//
-// plugin badge text
+// Etc...
//
-idStore.on('update', updateBadge)
-
-function updateBadge (state) {
- var label = ''
- var unconfTxs = controller.configManager.unconfirmedTxs()
- var unconfTxLen = Object.keys(unconfTxs).length
- var unconfMsgs = messageManager.unconfirmedMsgs()
- var unconfMsgLen = Object.keys(unconfMsgs).length
- var count = unconfTxLen + unconfMsgLen
- if (count) {
- label = String(count)
- }
- extension.browserAction.setBadgeText({ text: label })
- extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
+// popup trigger
+function triggerUi () {
+ if (!popupIsOpen) notification.show()
}
-function loadData () {
- var oldData = getOldStyleData()
- var newData
- try {
- newData = JSON.parse(window.localStorage[STORAGE_KEY])
- } catch (e) {}
-
- var data = extend({
- meta: {
- version: 0,
- },
- data: {
- config: {
- provider: {
- type: 'testnet',
- },
- },
- },
- }, oldData || null, newData || null)
- return data
-}
-
-function getOldStyleData () {
- var config, wallet, seedWords
-
- var result = {
- meta: { version: 0 },
- data: {},
+// On first install, open a window to MetaMask website to how-it-works.
+extension.runtime.onInstalled.addListener(function (details) {
+ if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
+ extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
}
-
- try {
- config = JSON.parse(window.localStorage['config'])
- result.data.config = config
- } catch (e) {}
- try {
- wallet = JSON.parse(window.localStorage['lightwallet'])
- result.data.wallet = wallet
- } catch (e) {}
- try {
- seedWords = window.localStorage['seedWords']
- result.data.seedWords = seedWords
- } catch (e) {}
-
- return result
-}
-
-function setData (data) {
- window.localStorage[STORAGE_KEY] = JSON.stringify(data)
-}
-
-function noop () {}
+})
diff --git a/app/scripts/chromereload.js b/app/scripts/chromereload.js
index 88333ba8a..f0bae403c 100644
--- a/app/scripts/chromereload.js
+++ b/app/scripts/chromereload.js
@@ -324,13 +324,13 @@ window.LiveReloadOptions = { host: 'localhost' };
this.pluginIdentifiers = {}
this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.location.href.match(/LR-verbose/) ? this.window.console : {
log: function () {},
- error: this.window.console.error.bind(this.window.console),
+ error: console.error,
} : {
log: function () {},
error: function () {},
}
if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) {
- this.console.error('LiveReload disabled because the browser does not seem to support web sockets')
+ console.error('LiveReload disabled because the browser does not seem to support web sockets')
return
}
if ('LiveReloadOptions' in window) {
@@ -344,7 +344,7 @@ window.LiveReloadOptions = { host: 'localhost' };
} else {
this.options = Options.extract(this.window.document)
if (!this.options) {
- this.console.error('LiveReload disabled because it could not find its own <SCRIPT> tag')
+ console.error('LiveReload disabled because it could not find its own <SCRIPT> tag')
return
}
}
diff --git a/app/scripts/config.js b/app/scripts/config.js
index 04e2907d4..b4541a04a 100644
--- a/app/scripts/config.js
+++ b/app/scripts/config.js
@@ -1,13 +1,14 @@
-const MAINET_RPC_URL = 'https://mainnet.infura.io/'
-const TESTNET_RPC_URL = 'https://morden.infura.io/'
+const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
+const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask'
const DEFAULT_RPC_URL = TESTNET_RPC_URL
-global.METAMASK_DEBUG = false
+global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
module.exports = {
network: {
default: DEFAULT_RPC_URL,
mainnet: MAINET_RPC_URL,
testnet: TESTNET_RPC_URL,
+ morden: TESTNET_RPC_URL,
},
}
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index de2cf263b..ab64dc9fa 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -1,11 +1,12 @@
const LocalMessageDuplexStream = require('post-message-stream')
+const PongStream = require('ping-pong-stream/pong')
const PortStream = require('./lib/port-stream.js')
const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('./lib/extension')
const fs = require('fs')
const path = require('path')
-const inpageText = fs.readFileSync(path.join(__dirname + '/inpage.js')).toString()
+const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
@@ -19,9 +20,8 @@ if (shouldInjectWeb3()) {
setupStreams()
}
-function setupInjection(){
+function setupInjection () {
try {
-
// inject in-page script
var scriptTag = document.createElement('script')
scriptTag.src = extension.extension.getURL('scripts/inpage.js')
@@ -30,41 +30,53 @@ function setupInjection(){
var container = document.head || document.documentElement
// append as first child
container.insertBefore(scriptTag, container.children[0])
-
} catch (e) {
console.error('Metamask injection failed.', e)
}
}
-function setupStreams(){
-
+function setupStreams () {
// setup communication to page and plugin
var pageStream = new LocalMessageDuplexStream({
name: 'contentscript',
target: 'inpage',
})
- pageStream.on('error', console.error.bind(console))
+ pageStream.on('error', console.error)
var pluginPort = extension.runtime.connect({name: 'contentscript'})
var pluginStream = new PortStream(pluginPort)
- pluginStream.on('error', console.error.bind(console))
+ pluginStream.on('error', console.error)
// forward communication plugin->inpage
pageStream.pipe(pluginStream).pipe(pageStream)
- // connect contentscript->inpage reload stream
+ // setup local multistream channels
var mx = ObjectMultiplex()
- mx.on('error', console.error.bind(console))
- mx.pipe(pageStream)
- var reloadStream = mx.createStream('reload')
- reloadStream.on('error', console.error.bind(console))
+ mx.on('error', console.error)
+ mx.pipe(pageStream).pipe(mx)
- // if we lose connection with the plugin, trigger tab refresh
- pluginStream.on('close', function () {
- reloadStream.write({ method: 'reset' })
- })
+ // connect ping stream
+ var pongStream = new PongStream({ objectMode: true })
+ pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream)
+
+ // ignore unused channels (handled by background)
+ mx.ignoreStream('provider')
+ mx.ignoreStream('publicConfig')
+ mx.ignoreStream('reload')
+}
+
+function shouldInjectWeb3 () {
+ return isAllowedSuffix(window.location.href)
}
-function shouldInjectWeb3(){
- var shouldInject = (window.location.href.indexOf('.pdf') === -1)
- return shouldInject
+function isAllowedSuffix (testCase) {
+ var prohibitedTypes = ['xml', 'pdf']
+ var currentUrl = window.location.href
+ var currentRegex
+ for (let i = 0; i < prohibitedTypes.length; i++) {
+ currentRegex = new RegExp(`\.${prohibitedTypes[i]}$`)
+ if (currentRegex.test(currentUrl)) {
+ return false
+ }
+ }
+ return true
}
diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js
new file mode 100644
index 000000000..3196981ba
--- /dev/null
+++ b/app/scripts/first-time-state.js
@@ -0,0 +1,11 @@
+//
+// The default state of MetaMask
+//
+
+module.exports = {
+ config: {
+ provider: {
+ type: 'testnet',
+ },
+ },
+} \ No newline at end of file
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index 28a1223ac..419f78cd6 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -2,6 +2,8 @@
cleanContextForImports()
require('web3/dist/web3.min.js')
const LocalMessageDuplexStream = require('post-message-stream')
+// const PingStream = require('ping-pong-stream/ping')
+// const endOfStream = require('end-of-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
@@ -29,15 +31,26 @@ web3.setProvider = function () {
console.log('MetaMask - overrode web3.setProvider')
}
console.log('MetaMask - injected web3')
+// export global web3, with usage-detection reload fn
+var triggerReload = setupDappAutoReload(web3)
-//
-// export global web3 with auto dapp reload
-//
-
+// listen for reset requests from metamask
var reloadStream = inpageProvider.multiStream.createStream('reload')
-setupDappAutoReload(web3, reloadStream)
+reloadStream.once('data', triggerReload)
+
+// setup ping timeout autoreload
+// LocalMessageDuplexStream does not self-close, so reload if pingStream fails
+// var pingChannel = inpageProvider.multiStream.createStream('pingpong')
+// var pingStream = new PingStream({ objectMode: true })
+// wait for first successful reponse
+
+// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully
+// metamaskStream.once('data', function(){
+// pingStream.pipe(pingChannel).pipe(pingStream)
+// })
+// endOfStream(pingStream, triggerReload)
-// set web3 defaultAcount
+// set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress
})
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
new file mode 100644
index 000000000..20af221ff
--- /dev/null
+++ b/app/scripts/keyring-controller.js
@@ -0,0 +1,554 @@
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+const bip39 = require('bip39')
+const EventEmitter = require('events').EventEmitter
+const ObservableStore = require('obs-store')
+const filter = require('promise-filter')
+const encryptor = require('browser-passworder')
+const normalizeAddress = require('./lib/sig-util').normalize
+// Keyrings:
+const SimpleKeyring = require('./keyrings/simple')
+const HdKeyring = require('./keyrings/hd')
+const keyringTypes = [
+ SimpleKeyring,
+ HdKeyring,
+]
+
+class KeyringController extends EventEmitter {
+
+ // PUBLIC METHODS
+ //
+ // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
+ // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
+ //
+ // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.
+
+ constructor (opts) {
+ super()
+ const initState = opts.initState || {}
+ this.keyringTypes = keyringTypes
+ this.store = new ObservableStore(initState)
+ this.memStore = new ObservableStore({
+ isUnlocked: false,
+ keyringTypes: this.keyringTypes.map(krt => krt.type),
+ keyrings: [],
+ identities: {},
+ })
+ this.ethStore = opts.ethStore
+ this.encryptor = encryptor
+ this.keyrings = []
+ this.getNetwork = opts.getNetwork
+ }
+
+ // Full Update
+ // returns Promise( @object state )
+ //
+ // Emits the `update` event and
+ // returns a Promise that resolves to the current state.
+ //
+ // Frequently used to end asynchronous chains in this class,
+ // indicating consumers can often either listen for updates,
+ // or accept a state-resolving promise to consume their results.
+ //
+ // Not all methods end with this, that might be a nice refactor.
+ fullUpdate () {
+ this.emit('update')
+ return Promise.resolve(this.memStore.getState())
+ }
+
+ // Create New Vault And Keychain
+ // @string password - The password to encrypt the vault with
+ //
+ // returns Promise( @object state )
+ //
+ // Destroys any old encrypted storage,
+ // creates a new encrypted store with the given password,
+ // randomly creates a new HD wallet with 1 account,
+ // faucets that account on the testnet.
+ createNewVaultAndKeychain (password) {
+ return this.persistAllKeyrings(password)
+ .then(this.createFirstKeyTree.bind(this))
+ .then(this.fullUpdate.bind(this))
+ }
+
+ // CreateNewVaultAndRestore
+ // @string password - The password to encrypt the vault with
+ // @string seed - The BIP44-compliant seed phrase.
+ //
+ // returns Promise( @object state )
+ //
+ // Destroys any old encrypted storage,
+ // creates a new encrypted store with the given password,
+ // creates a new HD wallet from the given seed with 1 account.
+ createNewVaultAndRestore (password, seed) {
+ if (typeof password !== 'string') {
+ return Promise.reject('Password must be text.')
+ }
+
+ if (!bip39.validateMnemonic(seed)) {
+ return Promise.reject('Seed phrase is invalid.')
+ }
+
+ this.clearKeyrings()
+
+ return this.persistAllKeyrings(password)
+ .then(() => {
+ return this.addNewKeyring('HD Key Tree', {
+ mnemonic: seed,
+ numberOfAccounts: 1,
+ })
+ })
+ .then((firstKeyring) => {
+ return firstKeyring.getAccounts()
+ })
+ .then((accounts) => {
+ const firstAccount = accounts[0]
+ if (!firstAccount) throw new Error('KeyringController - First Account not found.')
+ const hexAccount = normalizeAddress(firstAccount)
+ this.emit('newAccount', hexAccount)
+ return this.setupAccounts(accounts)
+ })
+ .then(this.persistAllKeyrings.bind(this, password))
+ .then(this.fullUpdate.bind(this))
+ }
+
+ // Set Locked
+ // returns Promise( @object state )
+ //
+ // This method deallocates all secrets, and effectively locks metamask.
+ setLocked () {
+ // set locked
+ this.password = null
+ this.memStore.updateState({ isUnlocked: false })
+ // remove keyrings
+ this.keyrings = []
+ this._updateMemStoreKeyrings()
+ return this.fullUpdate()
+ }
+
+ // Submit Password
+ // @string password
+ //
+ // returns Promise( @object state )
+ //
+ // Attempts to decrypt the current vault and load its keyrings
+ // into memory.
+ //
+ // Temporarily also migrates any old-style vaults first, as well.
+ // (Pre MetaMask 3.0.0)
+ submitPassword (password) {
+ return this.unlockKeyrings(password)
+ .then((keyrings) => {
+ this.keyrings = keyrings
+ return this.fullUpdate()
+ })
+ }
+
+ // Add New Keyring
+ // @string type
+ // @object opts
+ //
+ // returns Promise( @Keyring keyring )
+ //
+ // Adds a new Keyring of the given `type` to the vault
+ // and the current decrypted Keyrings array.
+ //
+ // All Keyring classes implement a unique `type` string,
+ // and this is used to retrieve them from the keyringTypes array.
+ addNewKeyring (type, opts) {
+ const Keyring = this.getKeyringClassForType(type)
+ const keyring = new Keyring(opts)
+ return keyring.deserialize(opts)
+ .then(() => {
+ return keyring.getAccounts()
+ })
+ .then((accounts) => {
+ this.keyrings.push(keyring)
+ return this.setupAccounts(accounts)
+ })
+ .then(() => this.persistAllKeyrings())
+ .then(() => this.fullUpdate())
+ .then(() => {
+ return keyring
+ })
+ }
+
+ // Add New Account
+ // @number keyRingNum
+ //
+ // returns Promise( @object state )
+ //
+ // Calls the `addAccounts` method on the Keyring
+ // in the kryings array at index `keyringNum`,
+ // and then saves those changes.
+ addNewAccount (selectedKeyring) {
+ return selectedKeyring.addAccounts(1)
+ .then(this.setupAccounts.bind(this))
+ .then(this.persistAllKeyrings.bind(this))
+ .then(this.fullUpdate.bind(this))
+ }
+
+ // Save Account Label
+ // @string account
+ // @string label
+ //
+ // returns Promise( @string label )
+ //
+ // Persists a nickname equal to `label` for the specified account.
+ saveAccountLabel (account, label) {
+ try {
+ const hexAddress = normalizeAddress(account)
+ // update state on diskStore
+ const state = this.store.getState()
+ const walletNicknames = state.walletNicknames || {}
+ walletNicknames[hexAddress] = label
+ this.store.updateState({ walletNicknames })
+ // update state on memStore
+ const identities = this.memStore.getState().identities
+ identities[hexAddress].name = label
+ this.memStore.updateState({ identities })
+ return Promise.resolve(label)
+ } catch (err) {
+ return Promise.reject(err)
+ }
+ }
+
+ // Export Account
+ // @string address
+ //
+ // returns Promise( @string privateKey )
+ //
+ // Requests the private key from the keyring controlling
+ // the specified address.
+ //
+ // Returns a Promise that may resolve with the private key string.
+ exportAccount (address) {
+ try {
+ return this.getKeyringForAccount(address)
+ .then((keyring) => {
+ return keyring.exportAccount(normalizeAddress(address))
+ })
+ } catch (e) {
+ return Promise.reject(e)
+ }
+ }
+
+
+ // SIGNING METHODS
+ //
+ // This method signs tx and returns a promise for
+ // TX Manager to update the state after signing
+
+ signTransaction (ethTx, _fromAddress) {
+ const fromAddress = normalizeAddress(_fromAddress)
+ return this.getKeyringForAccount(fromAddress)
+ .then((keyring) => {
+ return keyring.signTransaction(fromAddress, ethTx)
+ })
+ }
+
+ // Sign Message
+ // @object msgParams
+ //
+ // returns Promise(@buffer rawSig)
+ //
+ // Attempts to sign the provided @object msgParams.
+ signMessage (msgParams) {
+ const address = normalizeAddress(msgParams.from)
+ return this.getKeyringForAccount(address)
+ .then((keyring) => {
+ return keyring.signMessage(address, msgParams.data)
+ })
+ }
+
+ // PRIVATE METHODS
+ //
+ // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
+ // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
+
+ // Create First Key Tree
+ // returns @Promise
+ //
+ // Clears the vault,
+ // creates a new one,
+ // creates a random new HD Keyring with 1 account,
+ // makes that account the selected account,
+ // faucets that account on testnet,
+ // puts the current seed words into the state tree.
+ createFirstKeyTree () {
+ this.clearKeyrings()
+ return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
+ .then((keyring) => {
+ return keyring.getAccounts()
+ })
+ .then((accounts) => {
+ const firstAccount = accounts[0]
+ if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
+ const hexAccount = normalizeAddress(firstAccount)
+ this.emit('newAccount', hexAccount)
+ return this.setupAccounts(accounts)
+ })
+ .then(this.persistAllKeyrings.bind(this))
+ }
+
+ // Setup Accounts
+ // @array accounts
+ //
+ // returns @Promise(@object account)
+ //
+ // Initializes the provided account array
+ // Gives them numerically incremented nicknames,
+ // and adds them to the ethStore for regular balance checking.
+ setupAccounts (accounts) {
+ return this.getAccounts()
+ .then((loadedAccounts) => {
+ const arr = accounts || loadedAccounts
+ return Promise.all(arr.map((account) => {
+ return this.getBalanceAndNickname(account)
+ }))
+ })
+ }
+
+ // Get Balance And Nickname
+ // @string account
+ //
+ // returns Promise( @string label )
+ //
+ // Takes an account address and an iterator representing
+ // the current number of named accounts.
+ getBalanceAndNickname (account) {
+ if (!account) {
+ throw new Error('Problem loading account.')
+ }
+ const address = normalizeAddress(account)
+ this.ethStore.addAccount(address)
+ return this.createNickname(address)
+ }
+
+ // Create Nickname
+ // @string address
+ //
+ // returns Promise( @string label )
+ //
+ // Takes an address, and assigns it an incremented nickname, persisting it.
+ createNickname (address) {
+ const hexAddress = normalizeAddress(address)
+ const identities = this.memStore.getState().identities
+ const currentIdentityCount = Object.keys(identities).length + 1
+ const nicknames = this.store.getState().walletNicknames || {}
+ const existingNickname = nicknames[hexAddress]
+ const name = existingNickname || `Account ${currentIdentityCount}`
+ identities[hexAddress] = {
+ address: hexAddress,
+ name,
+ }
+ this.memStore.updateState({ identities })
+ return this.saveAccountLabel(hexAddress, name)
+ }
+
+ // Persist All Keyrings
+ // @password string
+ //
+ // returns Promise
+ //
+ // Iterates the current `keyrings` array,
+ // serializes each one into a serialized array,
+ // encrypts that array with the provided `password`,
+ // and persists that encrypted string to storage.
+ persistAllKeyrings (password = this.password) {
+ if (typeof password === 'string') {
+ this.password = password
+ this.memStore.updateState({ isUnlocked: true })
+ }
+ return Promise.all(this.keyrings.map((keyring) => {
+ return Promise.all([keyring.type, keyring.serialize()])
+ .then((serializedKeyringArray) => {
+ // Label the output values on each serialized Keyring:
+ return {
+ type: serializedKeyringArray[0],
+ data: serializedKeyringArray[1],
+ }
+ })
+ }))
+ .then((serializedKeyrings) => {
+ return this.encryptor.encrypt(this.password, serializedKeyrings)
+ })
+ .then((encryptedString) => {
+ this.store.updateState({ vault: encryptedString })
+ return true
+ })
+ }
+
+ // Unlock Keyrings
+ // @string password
+ //
+ // returns Promise( @array keyrings )
+ //
+ // Attempts to unlock the persisted encrypted storage,
+ // initializing the persisted keyrings to RAM.
+ unlockKeyrings (password) {
+ const encryptedVault = this.store.getState().vault
+ if (!encryptedVault) {
+ throw new Error('Cannot unlock without a previous vault.')
+ }
+
+ return this.encryptor.decrypt(password, encryptedVault)
+ .then((vault) => {
+ this.password = password
+ this.memStore.updateState({ isUnlocked: true })
+ vault.forEach(this.restoreKeyring.bind(this))
+ return this.keyrings
+ })
+ }
+
+ // Restore Keyring
+ // @object serialized
+ //
+ // returns Promise( @Keyring deserialized )
+ //
+ // Attempts to initialize a new keyring from the provided
+ // serialized payload.
+ //
+ // On success, returns the resulting @Keyring instance.
+ restoreKeyring (serialized) {
+ const { type, data } = serialized
+
+ const Keyring = this.getKeyringClassForType(type)
+ const keyring = new Keyring()
+ return keyring.deserialize(data)
+ .then(() => {
+ return keyring.getAccounts()
+ })
+ .then((accounts) => {
+ return this.setupAccounts(accounts)
+ })
+ .then(() => {
+ this.keyrings.push(keyring)
+ this._updateMemStoreKeyrings()
+ return keyring
+ })
+ }
+
+ // Get Keyring Class For Type
+ // @string type
+ //
+ // Returns @class Keyring
+ //
+ // Searches the current `keyringTypes` array
+ // for a Keyring class whose unique `type` property
+ // matches the provided `type`,
+ // returning it if it exists.
+ getKeyringClassForType (type) {
+ return this.keyringTypes.find(kr => kr.type === type)
+ }
+
+ getKeyringsByType (type) {
+ return this.keyrings.filter((keyring) => keyring.type === type)
+ }
+
+ // Get Accounts
+ // returns Promise( @Array[ @string accounts ] )
+ //
+ // Returns the public addresses of all current accounts
+ // managed by all currently unlocked keyrings.
+ getAccounts () {
+ const keyrings = this.keyrings || []
+ return Promise.all(keyrings.map(kr => kr.getAccounts()))
+ .then((keyringArrays) => {
+ return keyringArrays.reduce((res, arr) => {
+ return res.concat(arr)
+ }, [])
+ })
+ }
+
+ // Get Keyring For Account
+ // @string address
+ //
+ // returns Promise(@Keyring keyring)
+ //
+ // Returns the currently initialized keyring that manages
+ // the specified `address` if one exists.
+ getKeyringForAccount (address) {
+ const hexed = normalizeAddress(address)
+
+ return Promise.all(this.keyrings.map((keyring) => {
+ return Promise.all([
+ keyring,
+ keyring.getAccounts(),
+ ])
+ }))
+ .then(filter((candidate) => {
+ const accounts = candidate[1].map(normalizeAddress)
+ return accounts.includes(hexed)
+ }))
+ .then((winners) => {
+ if (winners && winners.length > 0) {
+ return winners[0][0]
+ } else {
+ throw new Error('No keyring found for the requested account.')
+ }
+ })
+ }
+
+ // Display For Keyring
+ // @Keyring keyring
+ //
+ // returns Promise( @Object { type:String, accounts:Array } )
+ //
+ // Is used for adding the current keyrings to the state object.
+ displayForKeyring (keyring) {
+ return keyring.getAccounts()
+ .then((accounts) => {
+ return {
+ type: keyring.type,
+ accounts: accounts,
+ }
+ })
+ }
+
+ // Add Gas Buffer
+ // @string gas (as hexadecimal value)
+ //
+ // returns @string bufferedGas (as hexadecimal value)
+ //
+ // Adds a healthy buffer of gas to an initial gas estimate.
+ addGasBuffer (gas) {
+ const gasBuffer = new BN('100000', 10)
+ const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
+ const correct = bnGas.add(gasBuffer)
+ return ethUtil.addHexPrefix(correct.toString(16))
+ }
+
+ // Clear Keyrings
+ //
+ // Deallocates all currently managed keyrings and accounts.
+ // Used before initializing a new vault.
+ clearKeyrings () {
+ let accounts
+ try {
+ accounts = Object.keys(this.ethStore.getState())
+ } catch (e) {
+ accounts = []
+ }
+ accounts.forEach((address) => {
+ this.ethStore.removeAccount(address)
+ })
+
+ // clear keyrings from memory
+ this.keyrings = []
+ this.memStore.updateState({
+ keyrings: [],
+ identities: {},
+ })
+ }
+
+ _updateMemStoreKeyrings() {
+ Promise.all(this.keyrings.map(this.displayForKeyring))
+ .then((keyrings) => {
+ this.memStore.updateState({ keyrings })
+ })
+ }
+
+}
+
+module.exports = KeyringController
diff --git a/app/scripts/keyrings/hd.js b/app/scripts/keyrings/hd.js
new file mode 100644
index 000000000..3a66f7868
--- /dev/null
+++ b/app/scripts/keyrings/hd.js
@@ -0,0 +1,125 @@
+const EventEmitter = require('events').EventEmitter
+const hdkey = require('ethereumjs-wallet/hdkey')
+const bip39 = require('bip39')
+const ethUtil = require('ethereumjs-util')
+
+// *Internal Deps
+const sigUtil = require('../lib/sig-util')
+
+// Options:
+const hdPathString = `m/44'/60'/0'/0`
+const type = 'HD Key Tree'
+
+class HdKeyring extends EventEmitter {
+
+ /* PUBLIC METHODS */
+
+ constructor (opts = {}) {
+ super()
+ this.type = type
+ this.deserialize(opts)
+ }
+
+ serialize () {
+ return Promise.resolve({
+ mnemonic: this.mnemonic,
+ numberOfAccounts: this.wallets.length,
+ })
+ }
+
+ deserialize (opts = {}) {
+ this.opts = opts || {}
+ this.wallets = []
+ this.mnemonic = null
+ this.root = null
+
+ if (opts.mnemonic) {
+ this._initFromMnemonic(opts.mnemonic)
+ }
+
+ if (opts.numberOfAccounts) {
+ return this.addAccounts(opts.numberOfAccounts)
+ }
+
+ return Promise.resolve([])
+ }
+
+ addAccounts (numberOfAccounts = 1) {
+ if (!this.root) {
+ this._initFromMnemonic(bip39.generateMnemonic())
+ }
+
+ const oldLen = this.wallets.length
+ const newWallets = []
+ for (let i = oldLen; i < numberOfAccounts + oldLen; i++) {
+ const child = this.root.deriveChild(i)
+ const wallet = child.getWallet()
+ newWallets.push(wallet)
+ this.wallets.push(wallet)
+ }
+ const hexWallets = newWallets.map(w => w.getAddress().toString('hex'))
+ return Promise.resolve(hexWallets)
+ }
+
+ getAccounts () {
+ return Promise.resolve(this.wallets.map(w => w.getAddress().toString('hex')))
+ }
+
+ // tx is an instance of the ethereumjs-transaction class.
+ signTransaction (address, tx) {
+ const wallet = this._getWalletForAccount(address)
+ var privKey = wallet.getPrivateKey()
+ tx.sign(privKey)
+ return Promise.resolve(tx)
+ }
+
+ // For eth_sign, we need to sign transactions:
+ // hd
+ signMessage (withAccount, data) {
+ const wallet = this._getWalletForAccount(withAccount)
+ const message = ethUtil.stripHexPrefix(data)
+ var privKey = wallet.getPrivateKey()
+ var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
+ var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
+ return Promise.resolve(rawMsgSig)
+ }
+
+ // For eth_sign, we need to sign transactions:
+ newGethSignMessage (withAccount, msgHex) {
+ const wallet = this._getWalletForAccount(withAccount)
+ const privKey = wallet.getPrivateKey()
+ const msgBuffer = ethUtil.toBuffer(msgHex)
+ const msgHash = ethUtil.hashPersonalMessage(msgBuffer)
+ const msgSig = ethUtil.ecsign(msgHash, privKey)
+ const rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
+ return Promise.resolve(rawMsgSig)
+ }
+
+ exportAccount (address) {
+ const wallet = this._getWalletForAccount(address)
+ return Promise.resolve(wallet.getPrivateKey().toString('hex'))
+ }
+
+
+ /* PRIVATE METHODS */
+
+ _initFromMnemonic (mnemonic) {
+ this.mnemonic = mnemonic
+ const seed = bip39.mnemonicToSeed(mnemonic)
+ this.hdWallet = hdkey.fromMasterSeed(seed)
+ this.root = this.hdWallet.derivePath(hdPathString)
+ }
+
+
+ _getWalletForAccount (account) {
+ const targetAddress = sigUtil.normalize(account)
+ return this.wallets.find((w) => {
+ const address = w.getAddress().toString('hex')
+ return ((address === targetAddress) ||
+ (sigUtil.normalize(address) === targetAddress))
+ })
+ }
+}
+
+HdKeyring.type = type
+module.exports = HdKeyring
diff --git a/app/scripts/keyrings/simple.js b/app/scripts/keyrings/simple.js
new file mode 100644
index 000000000..82881aa2d
--- /dev/null
+++ b/app/scripts/keyrings/simple.js
@@ -0,0 +1,100 @@
+const EventEmitter = require('events').EventEmitter
+const Wallet = require('ethereumjs-wallet')
+const ethUtil = require('ethereumjs-util')
+const type = 'Simple Key Pair'
+const sigUtil = require('../lib/sig-util')
+
+class SimpleKeyring extends EventEmitter {
+
+ /* PUBLIC METHODS */
+
+ constructor (opts) {
+ super()
+ this.type = type
+ this.opts = opts || {}
+ this.wallets = []
+ }
+
+ serialize () {
+ return Promise.resolve(this.wallets.map(w => w.getPrivateKey().toString('hex')))
+ }
+
+ deserialize (privateKeys = []) {
+ return new Promise((resolve, reject) => {
+ try {
+ this.wallets = privateKeys.map((privateKey) => {
+ const stripped = ethUtil.stripHexPrefix(privateKey)
+ const buffer = new Buffer(stripped, 'hex')
+ const wallet = Wallet.fromPrivateKey(buffer)
+ return wallet
+ })
+ } catch (e) {
+ reject(e)
+ }
+ resolve()
+ })
+ }
+
+ addAccounts (n = 1) {
+ var newWallets = []
+ for (var i = 0; i < n; i++) {
+ newWallets.push(Wallet.generate())
+ }
+ this.wallets = this.wallets.concat(newWallets)
+ const hexWallets = newWallets.map(w => ethUtil.bufferToHex(w.getAddress()))
+ return Promise.resolve(hexWallets)
+ }
+
+ getAccounts () {
+ return Promise.resolve(this.wallets.map(w => ethUtil.bufferToHex(w.getAddress())))
+ }
+
+ // tx is an instance of the ethereumjs-transaction class.
+ signTransaction (address, tx) {
+ const wallet = this._getWalletForAccount(address)
+ var privKey = wallet.getPrivateKey()
+ tx.sign(privKey)
+ return Promise.resolve(tx)
+ }
+
+ // For eth_sign, we need to sign transactions:
+ signMessage (withAccount, data) {
+ const wallet = this._getWalletForAccount(withAccount)
+ const message = ethUtil.stripHexPrefix(data)
+ var privKey = wallet.getPrivateKey()
+ var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
+ var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
+ return Promise.resolve(rawMsgSig)
+ }
+
+ // For eth_sign, we need to sign transactions:
+
+ newGethSignMessage (withAccount, msgHex) {
+ const wallet = this._getWalletForAccount(withAccount)
+ const privKey = wallet.getPrivateKey()
+ const msgBuffer = ethUtil.toBuffer(msgHex)
+ const msgHash = ethUtil.hashPersonalMessage(msgBuffer)
+ const msgSig = ethUtil.ecsign(msgHash, privKey)
+ const rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
+ return Promise.resolve(rawMsgSig)
+ }
+
+ exportAccount (address) {
+ const wallet = this._getWalletForAccount(address)
+ return Promise.resolve(wallet.getPrivateKey().toString('hex'))
+ }
+
+
+ /* PRIVATE METHODS */
+
+ _getWalletForAccount (account) {
+ const address = sigUtil.normalize(account)
+ let wallet = this.wallets.find(w => ethUtil.bufferToHex(w.getAddress()) === address)
+ if (!wallet) throw new Error('Simple Keyring - Unable to find matching address.')
+ return wallet
+ }
+
+}
+
+SimpleKeyring.type = type
+module.exports = SimpleKeyring
diff --git a/app/scripts/lib/auto-faucet.js b/app/scripts/lib/auto-faucet.js
index 59cf0ec20..1e86f735e 100644
--- a/app/scripts/lib/auto-faucet.js
+++ b/app/scripts/lib/auto-faucet.js
@@ -1,6 +1,9 @@
-var uri = 'https://faucet.metamask.io/'
+const uri = 'https://faucet.metamask.io/'
+const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
+const env = process.env.METAMASK_ENV
module.exports = function (address) {
+ if (METAMASK_DEBUG || env === 'test') return // Don't faucet in development or test
var http = new XMLHttpRequest()
var data = address
http.open('POST', uri, true)
diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js
index c4c8053f0..1302df35f 100644
--- a/app/scripts/lib/auto-reload.js
+++ b/app/scripts/lib/auto-reload.js
@@ -3,7 +3,7 @@ const ensnare = require('ensnare')
module.exports = setupDappAutoReload
-function setupDappAutoReload (web3, controlStream) {
+function setupDappAutoReload (web3) {
// export web3 as a global, checking for usage
var pageIsUsingWeb3 = false
var resetWasRequested = false
@@ -16,19 +16,18 @@ function setupDappAutoReload (web3, controlStream) {
global.web3 = web3
}))
- // listen for reset requests from metamask
- controlStream.once('data', function () {
+ return handleResetRequest
+
+ function handleResetRequest () {
resetWasRequested = true
// ignore if web3 was not used
if (!pageIsUsingWeb3) return
// reload after short timeout
- triggerReset()
- })
-
- // reload the page
- function triggerReset () {
- setTimeout(function () {
- global.location.reload()
- }, 500)
+ setTimeout(triggerReset, 500)
}
}
+
+// reload the page
+function triggerReset () {
+ global.location.reload()
+}
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index 715efb42e..6267eab68 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -1,11 +1,10 @@
-const Migrator = require('pojo-migrator')
const MetamaskConfig = require('../config.js')
-const migrations = require('./migrations')
-const rp = require('request-promise')
+const ethUtil = require('ethereumjs-util')
+const normalize = require('./sig-util').normalize
const TESTNET_RPC = MetamaskConfig.network.testnet
const MAINNET_RPC = MetamaskConfig.network.mainnet
-const txLimit = 40
+const MORDEN_RPC = MetamaskConfig.network.morden
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@@ -16,54 +15,21 @@ const txLimit = 40
*/
module.exports = ConfigManager
function ConfigManager (opts) {
- this.txLimit = txLimit
-
// ConfigManager is observable and will emit updates
this._subs = []
-
- /* The migrator exported on the config-manager
- * has two methods the user should be concerned with:
- *
- * getData(), which returns the app-consumable data object
- * saveData(), which persists the app-consumable data object.
- */
- this.migrator = new Migrator({
-
- // Migrations must start at version 1 or later.
- // They are objects with a `version` number
- // and a `migrate` function.
- //
- // The `migrate` function receives the previous
- // config data format, and returns the new one.
- migrations: migrations,
-
- // How to load initial config.
- // Includes step on migrating pre-pojo-migrator data.
- loadData: opts.loadData,
-
- // How to persist migrated config.
- setData: opts.setData,
- })
+ this.store = opts.store
}
ConfigManager.prototype.setConfig = function (config) {
- var data = this.migrator.getData()
+ var data = this.getData()
data.config = config
this.setData(data)
this._emitUpdates(config)
}
ConfigManager.prototype.getConfig = function () {
- var data = this.migrator.getData()
- if ('config' in data) {
- return data.config
- } else {
- return {
- provider: {
- type: 'testnet',
- },
- }
- }
+ var data = this.getData()
+ return data.config
}
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
@@ -97,19 +63,40 @@ ConfigManager.prototype.getProvider = function () {
}
ConfigManager.prototype.setData = function (data) {
- this.migrator.saveData(data)
+ this.store.putState(data)
}
ConfigManager.prototype.getData = function () {
- return this.migrator.getData()
+ return this.store.getState()
}
ConfigManager.prototype.setWallet = function (wallet) {
- var data = this.migrator.getData()
+ var data = this.getData()
data.wallet = wallet
this.setData(data)
}
+ConfigManager.prototype.setVault = function (encryptedString) {
+ var data = this.getData()
+ data.vault = encryptedString
+ this.setData(data)
+}
+
+ConfigManager.prototype.getVault = function () {
+ var data = this.getData()
+ return data.vault
+}
+
+ConfigManager.prototype.getKeychains = function () {
+ return this.getData().keychains || []
+}
+
+ConfigManager.prototype.setKeychains = function (keychains) {
+ var data = this.getData()
+ data.keychains = keychains
+ this.setData(data)
+}
+
ConfigManager.prototype.getSelectedAccount = function () {
var config = this.getConfig()
return config.selectedAccount
@@ -117,26 +104,38 @@ ConfigManager.prototype.getSelectedAccount = function () {
ConfigManager.prototype.setSelectedAccount = function (address) {
var config = this.getConfig()
- config.selectedAccount = address
+ config.selectedAccount = ethUtil.addHexPrefix(address)
this.setConfig(config)
}
ConfigManager.prototype.getWallet = function () {
- return this.migrator.getData().wallet
+ return this.getData().wallet
}
// Takes a boolean
ConfigManager.prototype.setShowSeedWords = function (should) {
- var data = this.migrator.getData()
+ var data = this.getData()
data.showSeedWords = should
this.setData(data)
}
+
ConfigManager.prototype.getShouldShowSeedWords = function () {
- var data = this.migrator.getData()
+ var data = this.getData()
return data.showSeedWords
}
+ConfigManager.prototype.setSeedWords = function (words) {
+ var data = this.getData()
+ data.seedWords = words
+ this.setData(data)
+}
+
+ConfigManager.prototype.getSeedWords = function () {
+ var data = this.getData()
+ return data.seedWords
+}
+
ConfigManager.prototype.getCurrentRpcAddress = function () {
var provider = this.getProvider()
if (!provider) return null
@@ -148,21 +147,20 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
case 'testnet':
return TESTNET_RPC
+ case 'morden':
+ return MORDEN_RPC
+
default:
return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC
}
}
-ConfigManager.prototype.setData = function (data) {
- this.migrator.saveData(data)
-}
-
//
// Tx
//
ConfigManager.prototype.getTxList = function () {
- var data = this.migrator.getData()
+ var data = this.getData()
if (data.transactions !== undefined) {
return data.transactions
} else {
@@ -170,61 +168,12 @@ ConfigManager.prototype.getTxList = function () {
}
}
-ConfigManager.prototype.unconfirmedTxs = function () {
- var transactions = this.getTxList()
- return transactions.filter(tx => tx.status === 'unconfirmed')
- .reduce((result, tx) => { result[tx.id] = tx; return result }, {})
-}
-
-ConfigManager.prototype._saveTxList = function (txList) {
- var data = this.migrator.getData()
+ConfigManager.prototype.setTxList = function (txList) {
+ var data = this.getData()
data.transactions = txList
this.setData(data)
}
-ConfigManager.prototype.addTx = function (tx) {
- var transactions = this.getTxList()
- while (transactions.length > this.txLimit - 1) {
- transactions.shift()
- }
- transactions.push(tx)
- this._saveTxList(transactions)
-}
-
-ConfigManager.prototype.getTx = function (txId) {
- var transactions = this.getTxList()
- var matching = transactions.filter(tx => tx.id === txId)
- return matching.length > 0 ? matching[0] : null
-}
-
-ConfigManager.prototype.confirmTx = function (txId) {
- this._setTxStatus(txId, 'confirmed')
-}
-
-ConfigManager.prototype.rejectTx = function (txId) {
- this._setTxStatus(txId, 'rejected')
-}
-
-ConfigManager.prototype._setTxStatus = function (txId, status) {
- var tx = this.getTx(txId)
- tx.status = status
- this.updateTx(tx)
-}
-
-ConfigManager.prototype.updateTx = function (tx) {
- var transactions = this.getTxList()
- var found, index
- transactions.forEach((otherTx, i) => {
- if (otherTx.id === tx.id) {
- found = true
- index = i
- }
- })
- if (found) {
- transactions[index] = tx
- }
- this._saveTxList(transactions)
-}
// wallet nickname methods
@@ -235,13 +184,15 @@ ConfigManager.prototype.getWalletNicknames = function () {
}
ConfigManager.prototype.nicknameForWallet = function (account) {
+ const address = normalize(account)
const nicknames = this.getWalletNicknames()
- return nicknames[account]
+ return nicknames[address]
}
ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
+ const address = normalize(account)
const nicknames = this.getWalletNicknames()
- nicknames[account] = nickname
+ nicknames[address] = nickname
var data = this.getData()
data.walletNicknames = nicknames
this.setData(data)
@@ -249,6 +200,17 @@ ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
// observable
+ConfigManager.prototype.getSalt = function () {
+ var data = this.getData()
+ return data.salt
+}
+
+ConfigManager.prototype.setSalt = function (salt) {
+ var data = this.getData()
+ data.salt = salt
+ this.setData(data)
+}
+
ConfigManager.prototype.subscribe = function (fn) {
this._subs.push(fn)
var unsubscribe = this.unsubscribe.bind(this, fn)
@@ -266,110 +228,25 @@ ConfigManager.prototype._emitUpdates = function (state) {
})
}
-ConfigManager.prototype.setConfirmed = function (confirmed) {
+ConfigManager.prototype.getGasMultiplier = function () {
var data = this.getData()
- data.isConfirmed = confirmed
- this.setData(data)
+ return data.gasMultiplier
}
-ConfigManager.prototype.getConfirmed = function () {
+ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) {
var data = this.getData()
- return ('isConfirmed' in data) && data.isConfirmed
-}
-ConfigManager.prototype.setCurrentFiat = function (currency) {
- var data = this.getData()
- data.fiatCurrency = currency
- this.setData(data)
-}
-
-ConfigManager.prototype.getCurrentFiat = function () {
- var data = this.getData()
- return ('fiatCurrency' in data) && data.fiatCurrency
-}
-
-ConfigManager.prototype.updateConversionRate = function () {
- var data = this.getData()
- return rp(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`)
- .then((response) => {
- const parsedResponse = JSON.parse(response)
- this.setConversionPrice(parsedResponse.ticker.price)
- this.setConversionDate(parsedResponse.timestamp)
- }).catch((err) => {
- console.error('Error in conversion.', err)
- this.setConversionPrice(0)
- this.setConversionDate('N/A')
- })
-
-}
-
-ConfigManager.prototype.setConversionPrice = function (price) {
- var data = this.getData()
- data.conversionRate = Number(price)
+ data.gasMultiplier = gasMultiplier
this.setData(data)
}
-ConfigManager.prototype.setConversionDate = function (datestring) {
+ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
var data = this.getData()
- data.conversionDate = datestring
+ data.lostAccounts = lostAccounts
this.setData(data)
}
-ConfigManager.prototype.getConversionRate = function () {
+ConfigManager.prototype.getLostAccounts = function () {
var data = this.getData()
- return (('conversionRate' in data) && data.conversionRate) || 0
-}
-
-ConfigManager.prototype.getConversionDate = function () {
- var data = this.getData()
- return (('conversionDate' in data) && data.conversionDate) || 'N/A'
-}
-
-ConfigManager.prototype.setShouldntShowWarning = function () {
- var data = this.getData()
- if (data.isEthConfirmed) {
- data.isEthConfirmed = !data.isEthConfirmed
- } else {
- data.isEthConfirmed = true
- }
- this.setData(data)
-}
-
-ConfigManager.prototype.getShouldntShowWarning = function () {
- var data = this.getData()
- return ('isEthConfirmed' in data) && data.isEthConfirmed
-}
-
-ConfigManager.prototype.getShapeShiftTxList = function () {
- var data = this.getData()
- var shapeShiftTxList = data.shapeShiftTxList ? data.shapeShiftTxList : []
- shapeShiftTxList.forEach((tx) => {
- if (tx.response.status !== 'complete') {
- var requestListner = function (request) {
- tx.response = JSON.parse(this.responseText)
- if (tx.response.status === 'complete') {
- tx.time = new Date().getTime()
- }
- }
-
- var shapShiftReq = new XMLHttpRequest()
- shapShiftReq.addEventListener('load', requestListner)
- shapShiftReq.open('GET', `https://shapeshift.io/txStat/${tx.depositAddress}`, true)
- shapShiftReq.send()
- }
- })
- this.setData(data)
- return shapeShiftTxList
-}
-
-ConfigManager.prototype.createShapeShiftTx = function (depositAddress, depositType) {
- var data = this.getData()
-
- var shapeShiftTx = {depositAddress, depositType, key: 'shapeshift', time: new Date().getTime(), response: {}}
- if (!data.shapeShiftTxList) {
- data.shapeShiftTxList = [shapeShiftTx]
- } else {
- data.shapeShiftTxList.push(shapeShiftTx)
- }
- this.setData(data)
+ return data.lostAccounts || []
}
diff --git a/app/scripts/lib/controllers/currency.js b/app/scripts/lib/controllers/currency.js
new file mode 100644
index 000000000..c4904f8ac
--- /dev/null
+++ b/app/scripts/lib/controllers/currency.js
@@ -0,0 +1,70 @@
+const ObservableStore = require('obs-store')
+const extend = require('xtend')
+
+// every ten minutes
+const POLLING_INTERVAL = 600000
+
+class CurrencyController {
+
+ constructor (opts = {}) {
+ const initState = extend({
+ currentCurrency: 'USD',
+ conversionRate: 0,
+ conversionDate: 'N/A',
+ }, opts.initState)
+ this.store = new ObservableStore(initState)
+ }
+
+ //
+ // PUBLIC METHODS
+ //
+
+ getCurrentCurrency () {
+ return this.store.getState().currentCurrency
+ }
+
+ setCurrentCurrency (currentCurrency) {
+ this.store.updateState({ currentCurrency })
+ }
+
+ getConversionRate () {
+ return this.store.getState().conversionRate
+ }
+
+ setConversionRate (conversionRate) {
+ this.store.updateState({ conversionRate })
+ }
+
+ getConversionDate () {
+ return this.store.getState().conversionDate
+ }
+
+ setConversionDate (conversionDate) {
+ this.store.updateState({ conversionDate })
+ }
+
+ updateConversionRate () {
+ const currentCurrency = this.getCurrentCurrency()
+ return fetch(`https://www.cryptonator.com/api/ticker/eth-${currentCurrency}`)
+ .then(response => response.json())
+ .then((parsedResponse) => {
+ this.setConversionRate(Number(parsedResponse.ticker.price))
+ this.setConversionDate(Number(parsedResponse.timestamp))
+ }).catch((err) => {
+ console.warn('MetaMask - Failed to query currency conversion.')
+ this.setConversionRate(0)
+ this.setConversionDate('N/A')
+ })
+ }
+
+ scheduleConversionInterval () {
+ if (this.conversionInterval) {
+ clearInterval(this.conversionInterval)
+ }
+ this.conversionInterval = setInterval(() => {
+ this.updateConversionRate()
+ }, POLLING_INTERVAL)
+ }
+}
+
+module.exports = CurrencyController
diff --git a/app/scripts/lib/controllers/preferences.js b/app/scripts/lib/controllers/preferences.js
new file mode 100644
index 000000000..dc9464c4e
--- /dev/null
+++ b/app/scripts/lib/controllers/preferences.js
@@ -0,0 +1,33 @@
+const ObservableStore = require('obs-store')
+const normalizeAddress = require('../sig-util').normalize
+
+class PreferencesController {
+
+ constructor (opts = {}) {
+ const initState = opts.initState || {}
+ this.store = new ObservableStore(initState)
+ }
+
+ //
+ // PUBLIC METHODS
+ //
+
+ setSelectedAddress(_address) {
+ return new Promise((resolve, reject) => {
+ const address = normalizeAddress(_address)
+ this.store.updateState({ selectedAddress: address })
+ resolve()
+ })
+ }
+
+ getSelectedAddress(_address) {
+ return this.store.getState().selectedAddress
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+}
+
+module.exports = PreferencesController
diff --git a/app/scripts/lib/controllers/shapeshift.js b/app/scripts/lib/controllers/shapeshift.js
new file mode 100644
index 000000000..3d955c01f
--- /dev/null
+++ b/app/scripts/lib/controllers/shapeshift.js
@@ -0,0 +1,104 @@
+const ObservableStore = require('obs-store')
+const extend = require('xtend')
+
+// every three seconds when an incomplete tx is waiting
+const POLLING_INTERVAL = 3000
+
+class ShapeshiftController {
+
+ constructor (opts = {}) {
+ const initState = extend({
+ shapeShiftTxList: [],
+ }, opts.initState)
+ this.store = new ObservableStore(initState)
+ this.pollForUpdates()
+ }
+
+ //
+ // PUBLIC METHODS
+ //
+
+ getShapeShiftTxList () {
+ const shapeShiftTxList = this.store.getState().shapeShiftTxList
+ return shapeShiftTxList
+ }
+
+ getPendingTxs () {
+ const txs = this.getShapeShiftTxList()
+ const pending = txs.filter(tx => tx.response && tx.response.status !== 'complete')
+ return pending
+ }
+
+ pollForUpdates () {
+ const pendingTxs = this.getPendingTxs()
+
+ if (pendingTxs.length === 0) {
+ return
+ }
+
+ Promise.all(pendingTxs.map((tx) => {
+ return this.updateTx(tx)
+ }))
+ .then((results) => {
+ results.forEach(tx => this.saveTx(tx))
+ this.timeout = setTimeout(this.pollForUpdates.bind(this), POLLING_INTERVAL)
+ })
+ }
+
+ updateTx (tx) {
+ const url = `https://shapeshift.io/txStat/${tx.depositAddress}`
+ return fetch(url)
+ .then((response) => {
+ return response.json()
+ }).then((json) => {
+ tx.response = json
+ if (tx.response.status === 'complete') {
+ tx.time = new Date().getTime()
+ }
+ return tx
+ })
+ }
+
+ saveTx (tx) {
+ const { shapeShiftTxList } = this.store.getState()
+ const index = shapeShiftTxList.indexOf(tx)
+ if (index !== -1) {
+ shapeShiftTxList[index] = tx
+ this.store.updateState({ shapeShiftTxList })
+ }
+ }
+
+ removeShapeShiftTx (tx) {
+ const { shapeShiftTxList } = this.store.getState()
+ const index = shapeShiftTxList.indexOf(index)
+ if (index !== -1) {
+ shapeShiftTxList.splice(index, 1)
+ }
+ this.updateState({ shapeShiftTxList })
+ }
+
+ createShapeShiftTx (depositAddress, depositType) {
+ const state = this.store.getState()
+ let { shapeShiftTxList } = state
+
+ var shapeShiftTx = {
+ depositAddress,
+ depositType,
+ key: 'shapeshift',
+ time: new Date().getTime(),
+ response: {},
+ }
+
+ if (!shapeShiftTxList) {
+ shapeShiftTxList = [shapeShiftTx]
+ } else {
+ shapeShiftTxList.push(shapeShiftTx)
+ }
+
+ this.store.updateState({ shapeShiftTxList })
+ this.pollForUpdates()
+ }
+
+}
+
+module.exports = ShapeshiftController
diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js
new file mode 100644
index 000000000..8812a507b
--- /dev/null
+++ b/app/scripts/lib/eth-store.js
@@ -0,0 +1,132 @@
+/* Ethereum Store
+ *
+ * This module is responsible for tracking any number of accounts
+ * and caching their current balances & transaction counts.
+ *
+ * It also tracks transaction hashes, and checks their inclusion status
+ * on each new block.
+ */
+
+const async = require('async')
+const EthQuery = require('eth-query')
+const ObservableStore = require('obs-store')
+function noop() {}
+
+
+class EthereumStore extends ObservableStore {
+
+ constructor (opts = {}) {
+ super({
+ accounts: {},
+ transactions: {},
+ })
+ this._provider = opts.provider
+ this._query = new EthQuery(this._provider)
+ this._blockTracker = opts.blockTracker
+ // subscribe to latest block
+ this._blockTracker.on('block', this._updateForBlock.bind(this))
+ // blockTracker.currentBlock may be null
+ this._currentBlockNumber = this._blockTracker.currentBlock
+ }
+
+ //
+ // public
+ //
+
+ addAccount (address) {
+ const accounts = this.getState().accounts
+ accounts[address] = {}
+ this.updateState({ accounts })
+ if (!this._currentBlockNumber) return
+ this._updateAccount(address)
+ }
+
+ removeAccount (address) {
+ const accounts = this.getState().accounts
+ delete accounts[address]
+ this.updateState({ accounts })
+ }
+
+ addTransaction (txHash) {
+ const transactions = this.getState().transactions
+ transactions[txHash] = {}
+ this.updateState({ transactions })
+ if (!this._currentBlockNumber) return
+ this._updateTransaction(this._currentBlockNumber, txHash, noop)
+ }
+
+ removeTransaction (txHash) {
+ const transactions = this.getState().transactions
+ delete transactions[txHash]
+ this.updateState({ transactions })
+ }
+
+
+ //
+ // private
+ //
+
+ _updateForBlock (block) {
+ const blockNumber = '0x' + block.number.toString('hex')
+ this._currentBlockNumber = blockNumber
+ async.parallel([
+ this._updateAccounts.bind(this),
+ this._updateTransactions.bind(this, blockNumber),
+ ], (err) => {
+ if (err) return console.error(err)
+ this.emit('block', this.getState())
+ })
+ }
+
+ _updateAccounts (cb = noop) {
+ const accounts = this.getState().accounts
+ const addresses = Object.keys(accounts)
+ async.each(addresses, this._updateAccount.bind(this), cb)
+ }
+
+ _updateAccount (address, cb = noop) {
+ const accounts = this.getState().accounts
+ this._getAccount(address, (err, result) => {
+ if (err) return cb(err)
+ result.address = address
+ // only populate if the entry is still present
+ if (accounts[address]) {
+ accounts[address] = result
+ this.updateState({ accounts })
+ }
+ cb(null, result)
+ })
+ }
+
+ _updateTransactions (block, cb = noop) {
+ const transactions = this.getState().transactions
+ const txHashes = Object.keys(transactions)
+ async.each(txHashes, this._updateTransaction.bind(this, block), cb)
+ }
+
+ _updateTransaction (block, txHash, cb = noop) {
+ // would use the block here to determine how many confirmations the tx has
+ const transactions = this.getState().transactions
+ this._query.getTransaction(txHash, (err, result) => {
+ if (err) return cb(err)
+ // only populate if the entry is still present
+ if (transactions[txHash]) {
+ transactions[txHash] = result
+ this.updateState({ transactions })
+ }
+ cb(null, result)
+ })
+ }
+
+ _getAccount (address, cb = noop) {
+ const query = this._query
+ async.parallel({
+ balance: query.getBalance.bind(query, address),
+ nonce: query.getTransactionCount.bind(query, address),
+ code: query.getCode.bind(query, address),
+ }, cb)
+ }
+
+}
+
+module.exports = EthereumStore \ No newline at end of file
diff --git a/app/scripts/lib/extension-instance.js b/app/scripts/lib/extension-instance.js
index eb3b8a1e9..628b62e3f 100644
--- a/app/scripts/lib/extension-instance.js
+++ b/app/scripts/lib/extension-instance.js
@@ -42,10 +42,27 @@ function Extension () {
} catch (e) {}
try {
+ if (browser[api]) {
+ _this[api] = browser[api]
+ }
+ } catch (e) {}
+ try {
_this.api = browser.extension[api]
} catch (e) {}
-
})
+
+ try {
+ if (browser && browser.runtime) {
+ this.runtime = browser.runtime
+ }
+ } catch (e) {}
+
+ try {
+ if (browser && browser.browserAction) {
+ this.browserAction = browser.browserAction
+ }
+ } catch (e) {}
+
}
module.exports = Extension
diff --git a/app/scripts/lib/id-management.js b/app/scripts/lib/id-management.js
index 9b8ceb415..421f2105f 100644
--- a/app/scripts/lib/id-management.js
+++ b/app/scripts/lib/id-management.js
@@ -1,4 +1,13 @@
+/* ID Management
+ *
+ * This module exists to hold the decrypted credentials for the current session.
+ * It therefore exposes sign methods, because it is able to perform these
+ * with noa dditional authentication, because its very instantiation
+ * means the vault is unlocked.
+ */
+
const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
const Transaction = require('ethereumjs-tx')
module.exports = IdManagement
@@ -16,9 +25,15 @@ function IdManagement (opts) {
}
this.signTx = function (txParams) {
+ // calculate gas with custom gas multiplier
+ var gasMultiplier = this.configManager.getGasMultiplier() || 1
+ var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
+ txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
// normalize values
+
txParams.to = ethUtil.addHexPrefix(txParams.to)
- txParams.from = ethUtil.addHexPrefix(txParams.from)
+ txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase())
txParams.value = ethUtil.addHexPrefix(txParams.value)
txParams.data = ethUtil.addHexPrefix(txParams.data)
txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas)
@@ -43,7 +58,7 @@ function IdManagement (opts) {
this.signMsg = function (address, message) {
// sign message
- var privKeyHex = this.exportPrivateKey(address)
+ var privKeyHex = this.exportPrivateKey(address.toLowerCase())
var privKey = ethUtil.toBuffer(privKeyHex)
var msgSig = ethUtil.ecsign(new Buffer(message.replace('0x', ''), 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s))
diff --git a/app/scripts/lib/idStore-migrator.js b/app/scripts/lib/idStore-migrator.js
new file mode 100644
index 000000000..655aed0af
--- /dev/null
+++ b/app/scripts/lib/idStore-migrator.js
@@ -0,0 +1,80 @@
+const IdentityStore = require('./idStore')
+const HdKeyring = require('../keyrings/hd')
+const sigUtil = require('./sig-util')
+const normalize = sigUtil.normalize
+const denodeify = require('denodeify')
+
+module.exports = class IdentityStoreMigrator {
+
+ constructor ({ configManager }) {
+ this.configManager = configManager
+ const hasOldVault = this.hasOldVault()
+ if (!hasOldVault) {
+ this.idStore = new IdentityStore({ configManager })
+ }
+ }
+
+ migratedVaultForPassword (password) {
+ const hasOldVault = this.hasOldVault()
+ const configManager = this.configManager
+
+ if (!this.idStore) {
+ this.idStore = new IdentityStore({ configManager })
+ }
+
+ if (!hasOldVault) {
+ return Promise.resolve(null)
+ }
+
+ const idStore = this.idStore
+ const submitPassword = denodeify(idStore.submitPassword.bind(idStore))
+
+ return submitPassword(password)
+ .then(() => {
+ const serialized = this.serializeVault()
+ return this.checkForLostAccounts(serialized)
+ })
+ }
+
+ serializeVault () {
+ const mnemonic = this.idStore._idmgmt.getSeed()
+ const numberOfAccounts = this.idStore._getAddresses().length
+
+ return {
+ type: 'HD Key Tree',
+ data: { mnemonic, numberOfAccounts },
+ }
+ }
+
+ checkForLostAccounts (serialized) {
+ const hd = new HdKeyring()
+ return hd.deserialize(serialized.data)
+ .then((hexAccounts) => {
+ const newAccounts = hexAccounts.map(normalize)
+ const oldAccounts = this.idStore._getAddresses().map(normalize)
+ const lostAccounts = oldAccounts.reduce((result, account) => {
+ if (newAccounts.includes(account)) {
+ return result
+ } else {
+ result.push(account)
+ return result
+ }
+ }, [])
+
+ return {
+ serialized,
+ lostAccounts: lostAccounts.map((address) => {
+ return {
+ address,
+ privateKey: this.idStore.exportAccount(address),
+ }
+ }),
+ }
+ })
+ }
+
+ hasOldVault () {
+ const wallet = this.configManager.getWallet()
+ return wallet
+ }
+}
diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js
index 7ac71e409..7a6968c6c 100644
--- a/app/scripts/lib/idStore.js
+++ b/app/scripts/lib/idStore.js
@@ -1,18 +1,14 @@
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
-const async = require('async')
const ethUtil = require('ethereumjs-util')
-const EthQuery = require('eth-query')
-const LightwalletKeyStore = require('eth-lightwallet').keystore
+const KeyStore = require('eth-lightwallet').keystore
const clone = require('clone')
const extend = require('xtend')
-const createId = require('web3-provider-engine/util/random-id')
-const ethBinToOps = require('eth-bin-to-ops')
const autoFaucet = require('./auto-faucet')
-const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
const IdManagement = require('./id-management')
+
module.exports = IdentityStore
inherits(IdentityStore, EventEmitter)
@@ -33,28 +29,32 @@ function IdentityStore (opts = {}) {
selectedAddress: null,
identities: {},
}
-
// not part of serilized metamask state - only kept in memory
- this._unconfTxCbs = {}
- this._unconfMsgCbs = {}
}
//
// public
//
-IdentityStore.prototype.createNewVault = function (password, entropy, cb) {
+IdentityStore.prototype.createNewVault = function (password, cb) {
delete this._keyStore
+ var serializedKeystore = this.configManager.getWallet()
+
+ if (serializedKeystore) {
+ this.configManager.setData({})
+ }
- this._createIdmgmt(password, null, entropy, (err) => {
+ this.purgeCache()
+ this._createVault(password, null, (err) => {
if (err) return cb(err)
- this._loadIdentities()
- this._didUpdate()
this._autoFaucet()
this.configManager.setShowSeedWords(true)
var seedWords = this._idmgmt.getSeed()
+
+ this._loadIdentities()
+
cb(null, seedWords)
})
}
@@ -67,11 +67,12 @@ IdentityStore.prototype.recoverSeed = function (cb) {
}
IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) {
- this._createIdmgmt(password, seed, null, (err) => {
+ this.purgeCache()
+
+ this._createVault(password, seed, (err) => {
if (err) return cb(err)
this._loadIdentities()
- this._didUpdate()
cb(null, this.getState())
})
}
@@ -93,17 +94,8 @@ IdentityStore.prototype.getState = function () {
isInitialized: !!configManager.getWallet() && !seedWords,
isUnlocked: this._isUnlocked(),
seedWords: seedWords,
- isConfirmed: configManager.getConfirmed(),
- isEthConfirmed: configManager.getShouldntShowWarning(),
- unconfTxs: configManager.unconfirmedTxs(),
- transactions: configManager.getTxList(),
- unconfMsgs: messageManager.unconfirmedMsgs(),
- messages: messageManager.getMsgList(),
selectedAddress: configManager.getSelectedAccount(),
- shapeShiftTxList: configManager.getShapeShiftTxList(),
- currentFiat: configManager.getCurrentFiat(),
- conversionRate: configManager.getConversionRate(),
- conversionDate: configManager.getConversionDate(),
+ gasMultiplier: configManager.getGasMultiplier(),
}))
}
@@ -121,7 +113,7 @@ IdentityStore.prototype.getSelectedAddress = function () {
return configManager.getSelectedAccount()
}
-IdentityStore.prototype.setSelectedAddress = function (address, cb) {
+IdentityStore.prototype.setSelectedAddressSync = function (address) {
const configManager = this.configManager
if (!address) {
var addresses = this._getAddresses()
@@ -129,7 +121,12 @@ IdentityStore.prototype.setSelectedAddress = function (address, cb) {
}
configManager.setSelectedAccount(address)
- if (cb) return cb(null, address)
+ return address
+}
+
+IdentityStore.prototype.setSelectedAddress = function (address, cb) {
+ const resultAddress = this.setSelectedAddressSync(address)
+ if (cb) return cb(null, resultAddress)
}
IdentityStore.prototype.revealAccount = function (cb) {
@@ -139,6 +136,11 @@ IdentityStore.prototype.revealAccount = function (cb) {
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 1)
+ const addresses = keyStore.getAddresses()
+ const address = addresses[ addresses.length - 1 ]
+
+ this._ethStore.addAccount(ethUtil.addHexPrefix(address))
+
configManager.setWallet(keyStore.serialize())
this._loadIdentities()
@@ -183,192 +185,10 @@ IdentityStore.prototype.submitPassword = function (password, cb) {
IdentityStore.prototype.exportAccount = function (address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
- cb(null, privateKey)
+ if (cb) cb(null, privateKey)
+ return privateKey
}
-//
-// Transactions
-//
-
-// comes from dapp via zero-client hooked-wallet provider
-IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) {
- const configManager = this.configManager
- var self = this
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var txId = createId()
- txParams.metamaskId = txId
- txParams.metamaskNetworkId = self._currentState.network
- var txData = {
- id: txId,
- txParams: txParams,
- time: time,
- status: 'unconfirmed',
- }
-
- console.log('addUnconfirmedTransaction:', txData)
-
- // keep the onTxDoneCb around for after approval/denial (requires user interaction)
- // This onTxDoneCb fires completion to the Dapp's write operation.
- self._unconfTxCbs[txId] = onTxDoneCb
-
- var provider = self._ethStore._query.currentProvider
- var query = new EthQuery(provider)
-
- // calculate metadata for tx
- async.parallel([
- analyzeForDelegateCall,
- estimateGas,
- ], didComplete)
-
- // perform static analyis on the target contract code
- function analyzeForDelegateCall(cb){
- if (txParams.to) {
- query.getCode(txParams.to, function (err, result) {
- if (err) return cb(err)
- var code = ethUtil.toBuffer(result)
- if (code !== '0x') {
- var ops = ethBinToOps(code)
- var containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL')
- txData.containsDelegateCall = containsDelegateCall
- cb()
- } else {
- cb()
- }
- })
- } else {
- cb()
- }
- }
-
- function estimateGas(cb){
- query.estimateGas(txParams, function(err, result){
- if (err) return cb(err)
- txData.estimatedGas = result
- cb()
- })
- }
-
- function didComplete (err) {
- if (err) return cb(err)
- configManager.addTx(txData)
- // signal update
- self._didUpdate()
- // signal completion of add tx
- cb(null, txData)
- }
-}
-
-// comes from metamask ui
-IdentityStore.prototype.approveTransaction = function (txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // accept tx
- cb()
- approvalCb(null, true)
- // clean up
- configManager.confirmTx(txId)
- delete this._unconfTxCbs[txId]
- this._didUpdate()
-}
-
-// comes from metamask ui
-IdentityStore.prototype.cancelTransaction = function (txId) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- configManager.rejectTx(txId)
- delete this._unconfTxCbs[txId]
- this._didUpdate()
-}
-
-// performs the actual signing, no autofill of params
-IdentityStore.prototype.signTransaction = function (txParams, cb) {
- try {
- console.log('signing tx...', txParams)
- var rawTx = this._idmgmt.signTx(txParams)
- cb(null, rawTx)
- } catch (err) {
- cb(err)
- }
-}
-
-//
-// Messages
-//
-
-// comes from dapp via zero-client hooked-wallet provider
-IdentityStore.prototype.addUnconfirmedMessage = function (msgParams, cb) {
- // 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: 'unconfirmed',
- }
- messageManager.addMsg(msgData)
- console.log('addUnconfirmedMessage:', msgData)
-
- // keep the cb around for after approval (requires user interaction)
- // This cb fires completion to the Dapp's write operation.
- this._unconfMsgCbs[msgId] = cb
-
- // signal update
- this._didUpdate()
-
- return msgId
-}
-
-// comes from metamask ui
-IdentityStore.prototype.approveMessage = function (msgId, cb) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // accept msg
- cb()
- approvalCb(null, true)
- // clean up
- messageManager.confirmMsg(msgId)
- delete this._unconfMsgCbs[msgId]
- this._didUpdate()
-}
-
-// comes from metamask ui
-IdentityStore.prototype.cancelMessage = function (msgId) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- messageManager.rejectMsg(msgId)
- delete this._unconfTxCbs[msgId]
- this._didUpdate()
-}
-
-// performs the actual signing, no autofill of params
-IdentityStore.prototype.signMessage = function (msgParams, cb) {
- try {
- console.log('signing msg...', msgParams.data)
- var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
- if ('metamaskId' in msgParams) {
- var id = msgParams.metamaskId
- delete msgParams.metamaskId
-
- this.approveMessage(id, cb)
- } else {
- cb(null, rawMsg)
- }
- } catch (err) {
- cb(err)
- }
-}
-
-//
// private
//
@@ -389,9 +209,11 @@ IdentityStore.prototype._loadIdentities = function () {
var addresses = this._getAddresses()
addresses.forEach((address, i) => {
// // add to ethStore
- this._ethStore.addAccount(address)
+ if (this._ethStore) {
+ this._ethStore.addAccount(ethUtil.addHexPrefix(address))
+ }
// add to identities
- const defaultLabel = 'Wallet ' + (i + 1)
+ const defaultLabel = 'Account ' + (i + 1)
const nickname = configManager.nicknameForWallet(address)
var identity = {
name: nickname || defaultLabel,
@@ -408,7 +230,6 @@ IdentityStore.prototype.saveAccountLabel = function (account, label, cb) {
configManager.setNicknameForWallet(account, label)
this._loadIdentities()
cb(null, label)
- this._didUpdate()
}
// mayBeFauceting
@@ -432,76 +253,87 @@ IdentityStore.prototype._mayBeFauceting = function (i) {
//
IdentityStore.prototype.tryPassword = function (password, cb) {
- this._createIdmgmt(password, null, null, cb)
+ var serializedKeystore = this.configManager.getWallet()
+ var keyStore = KeyStore.deserialize(serializedKeystore)
+
+ keyStore.keyFromPassword(password, (err, pwDerivedKey) => {
+ if (err) return cb(err)
+
+ const isCorrect = keyStore.isDerivedKeyCorrect(pwDerivedKey)
+ if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))
+
+ this._keyStore = keyStore
+ this._createIdMgmt(pwDerivedKey)
+ cb()
+ })
}
-IdentityStore.prototype._createIdmgmt = function (password, seed, entropy, cb) {
- const configManager = this.configManager
- var keyStore = null
- LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => {
+IdentityStore.prototype._createVault = function (password, seedPhrase, cb) {
+ const opts = {
+ password,
+ hdPathString: this.hdPathString,
+ }
+
+ if (seedPhrase) {
+ opts.seedPhrase = seedPhrase
+ }
+
+ KeyStore.createVault(opts, (err, keyStore) => {
if (err) return cb(err)
- var serializedKeystore = configManager.getWallet()
-
- if (seed) {
- try {
- keyStore = this._restoreFromSeed(password, seed, derivedKey)
- } catch (e) {
- return cb(e)
- }
-
- // returning user, recovering from storage
- } else if (serializedKeystore) {
- keyStore = LightwalletKeyStore.deserialize(serializedKeystore)
- var isCorrect = keyStore.isDerivedKeyCorrect(derivedKey)
- if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))
-
- // first time here
- } else {
- keyStore = this._createFirstWallet(entropy, derivedKey)
- }
this._keyStore = keyStore
- this._idmgmt = new IdManagement({
- keyStore: keyStore,
- derivedKey: derivedKey,
- hdPathSTring: this.hdPathString,
- configManager: this.configManager,
- })
- cb()
+ keyStore.keyFromPassword(password, (err, derivedKey) => {
+ if (err) return cb(err)
+
+ this.purgeCache()
+
+ keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'})
+
+ this._createFirstWallet(derivedKey)
+ this._createIdMgmt(derivedKey)
+ this.setSelectedAddressSync()
+
+ cb()
+ })
})
}
-IdentityStore.prototype._restoreFromSeed = function (password, seed, derivedKey) {
- const configManager = this.configManager
- var keyStore = new LightwalletKeyStore(seed, derivedKey, this.hdPathString)
- keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'})
- keyStore.setDefaultHdDerivationPath(this.hdPathString)
+IdentityStore.prototype._createIdMgmt = function (derivedKey) {
+ this._idmgmt = new IdManagement({
+ keyStore: this._keyStore,
+ derivedKey: derivedKey,
+ configManager: this.configManager,
+ })
+}
- keyStore.generateNewAddress(derivedKey, 3)
- configManager.setWallet(keyStore.serialize())
- if (global.METAMASK_DEBUG) {
- console.log('restored from seed. saved to keystore')
+IdentityStore.prototype.purgeCache = function () {
+ this._currentState.identities = {}
+ let accounts
+ try {
+ accounts = Object.keys(this._ethStore._currentState.accounts)
+ } catch (e) {
+ accounts = []
}
- return keyStore
+ accounts.forEach((address) => {
+ this._ethStore.removeAccount(address)
+ })
}
-IdentityStore.prototype._createFirstWallet = function (entropy, derivedKey) {
- const configManager = this.configManager
- var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy)
- var keyStore = new LightwalletKeyStore(secretSeed, derivedKey, this.hdPathString)
- keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'})
+IdentityStore.prototype._createFirstWallet = function (derivedKey) {
+ const keyStore = this._keyStore
keyStore.setDefaultHdDerivationPath(this.hdPathString)
-
keyStore.generateNewAddress(derivedKey, 1)
- configManager.setWallet(keyStore.serialize())
- console.log('saved to keystore')
- return keyStore
+ this.configManager.setWallet(keyStore.serialize())
+ var addresses = keyStore.getAddresses()
+ this._ethStore.addAccount(ethUtil.addHexPrefix(addresses[0]))
}
// get addresses and normalize address hexString
IdentityStore.prototype._getAddresses = function () {
- return this._keyStore.getAddresses(this.hdPathString).map((address) => { return '0x' + address })
+ return this._keyStore.getAddresses(this.hdPathString).map((address) => {
+ return ethUtil.addHexPrefix(address)
+ })
}
IdentityStore.prototype._autoFaucet = function () {
@@ -510,5 +342,3 @@ IdentityStore.prototype._autoFaucet = function () {
}
// util
-
-function noop () {}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index 65354cd3d..92936de2f 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -1,7 +1,8 @@
-const Streams = require('mississippi')
-const ObjectMultiplex = require('./obj-multiplex')
+const pipe = require('pump')
const StreamProvider = require('web3-stream-provider')
-const RemoteStore = require('./remote-store.js').RemoteStore
+const LocalStorageStore = require('obs-store')
+const ObjectMultiplex = require('./obj-multiplex')
+const createRandomId = require('./random-id')
module.exports = MetamaskInpageProvider
@@ -9,64 +10,89 @@ function MetamaskInpageProvider (connectionStream) {
const self = this
// setup connectionStream multiplexing
- var multiStream = ObjectMultiplex()
- Streams.pipe(connectionStream, multiStream, connectionStream, function (err) {
- console.warn('MetamaskInpageProvider - lost connection to MetaMask')
- if (err) throw err
- })
- self.multiStream = multiStream
-
- // subscribe to metamask public config
- var publicConfigStore = remoteStoreWithLocalStorageCache('MetaMask-Config')
- var storeStream = publicConfigStore.createStream()
- Streams.pipe(storeStream, multiStream.createStream('publicConfig'), storeStream, function (err) {
- console.warn('MetamaskInpageProvider - lost connection to MetaMask publicConfig')
- if (err) throw err
- })
- self.publicConfigStore = publicConfigStore
+ var multiStream = self.multiStream = ObjectMultiplex()
+ pipe(
+ connectionStream,
+ multiStream,
+ connectionStream,
+ (err) => logStreamDisconnectWarning('MetaMask', err)
+ )
+
+ // subscribe to metamask public config (one-way)
+ self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
+ pipe(
+ multiStream.createStream('publicConfig'),
+ self.publicConfigStore,
+ (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
+ )
// connect to async provider
- var asyncProvider = new StreamProvider()
- Streams.pipe(asyncProvider, multiStream.createStream('provider'), asyncProvider, function (err) {
- console.warn('MetamaskInpageProvider - lost connection to MetaMask provider')
- if (err) throw err
- })
- asyncProvider.on('error', console.error.bind(console))
- self.asyncProvider = asyncProvider
+ const asyncProvider = self.asyncProvider = new StreamProvider()
+ pipe(
+ asyncProvider,
+ multiStream.createStream('provider'),
+ asyncProvider,
+ (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
+ )
+
+ self.idMap = {}
// handle sendAsync requests via asyncProvider
- self.sendAsync = function(payload, cb){
+ self.sendAsync = function (payload, cb) {
// rewrite request ids
- var request = jsonrpcMessageTransform(payload, (message) => {
- message.id = createRandomId()
+ var request = eachJsonMessage(payload, (message) => {
+ var newId = createRandomId()
+ self.idMap[newId] = message.id
+ message.id = newId
return message
})
// forward to asyncProvider
- asyncProvider.sendAsync(request, cb)
+ asyncProvider.sendAsync(request, function (err, res) {
+ if (err) return cb(err)
+ // transform messages to original ids
+ eachJsonMessage(res, (message) => {
+ var oldId = self.idMap[message.id]
+ delete self.idMap[message.id]
+ message.id = oldId
+ return message
+ })
+ cb(null, res)
+ })
}
}
MetamaskInpageProvider.prototype.send = function (payload) {
const self = this
-
+
let selectedAddress
let result = null
switch (payload.method) {
case 'eth_accounts':
// read from localStorage
- selectedAddress = self.publicConfigStore.get('selectedAddress')
+ selectedAddress = self.publicConfigStore.getState().selectedAddress
result = selectedAddress ? [selectedAddress] : []
break
case 'eth_coinbase':
// read from localStorage
- selectedAddress = self.publicConfigStore.get('selectedAddress')
- result = selectedAddress || '0x0000000000000000000000000000000000000000'
+ selectedAddress = self.publicConfigStore.getState().selectedAddress
+ result = selectedAddress
+ break
+
+ case 'eth_uninstallFilter':
+ self.sendAsync(payload, noop)
+ result = true
+ break
+
+ case 'net_version':
+ let networkVersion = self.publicConfigStore.getState().networkVersion
+ result = networkVersion
break
// throw not-supported Error
default:
- var message = 'The MetaMask Web3 object does not support synchronous methods. See https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#all-async---think-of-metamask-as-a-light-client for details.'
+ var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client'
+ var message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.`
throw new Error(message)
}
@@ -87,34 +113,22 @@ MetamaskInpageProvider.prototype.isConnected = function () {
return true
}
-// util
-
-function remoteStoreWithLocalStorageCache (storageKey) {
- // read local cache
- var initState = JSON.parse(localStorage[storageKey] || '{}')
- var store = new RemoteStore(initState)
- // cache the latest state locally
- store.subscribe(function (state) {
- localStorage[storageKey] = JSON.stringify(state)
- })
-
- return store
-}
+MetamaskInpageProvider.prototype.isMetaMask = true
-function createRandomId(){
- const extraDigits = 3
- // 13 time digits
- const datePart = new Date().getTime() * Math.pow(10, extraDigits)
- // 3 random digits
- const extraPart = Math.floor(Math.random() * Math.pow(10, extraDigits))
- // 16 digits
- return datePart + extraPart
-}
+// util
-function jsonrpcMessageTransform(payload, transformFn){
+function eachJsonMessage (payload, transformFn) {
if (Array.isArray(payload)) {
return payload.map(transformFn)
} else {
return transformFn(payload)
}
-} \ No newline at end of file
+}
+
+function logStreamDisconnectWarning(remoteLabel, err){
+ let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
+ if (err) warningMsg += '\n' + err.stack
+ console.warn(warningMsg)
+}
+
+function noop () {}
diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js
new file mode 100644
index 000000000..693fa8751
--- /dev/null
+++ b/app/scripts/lib/is-popup-or-notification.js
@@ -0,0 +1,8 @@
+module.exports = function isPopupOrNotification () {
+ const url = window.location.href
+ if (url.match(/popup.html$/)) {
+ return 'popup'
+ } else {
+ return 'notification'
+ }
+}
diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js
index b609b820e..ceaf8ee2f 100644
--- a/app/scripts/lib/message-manager.js
+++ b/app/scripts/lib/message-manager.js
@@ -1,61 +1,118 @@
-module.exports = new MessageManager()
+const EventEmitter = require('events')
+const ObservableStore = require('obs-store')
+const ethUtil = require('ethereumjs-util')
+const createId = require('./random-id')
-function MessageManager (opts) {
- this.messages = []
-}
-MessageManager.prototype.getMsgList = function () {
- return this.messages
-}
+module.exports = class MessageManager extends EventEmitter{
+ constructor (opts) {
+ super()
+ this.memStore = new ObservableStore({
+ unapprovedMsgs: {},
+ unapprovedMsgCount: 0,
+ })
+ this.messages = []
+ }
-MessageManager.prototype.unconfirmedMsgs = function () {
- var messages = this.getMsgList()
- return messages.filter(msg => msg.status === 'unconfirmed')
- .reduce((result, msg) => { result[msg.id] = msg; return result }, {})
-}
+ get unapprovedMsgCount () {
+ return Object.keys(this.getUnapprovedMsgs()).length
+ }
-MessageManager.prototype._saveMsgList = function (msgList) {
- this.messages = msgList
-}
+ getUnapprovedMsgs () {
+ return this.messages.filter(msg => msg.status === 'unapproved')
+ .reduce((result, msg) => { result[msg.id] = msg; return result }, {})
+ }
-MessageManager.prototype.addMsg = function (msg) {
- var messages = this.getMsgList()
- messages.push(msg)
- this._saveMsgList(messages)
-}
+ addUnapprovedMessage (msgParams) {
+ msgParams.data = normalizeMsgData(msgParams.data)
+ // 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',
+ }
+ this.addMsg(msgData)
-MessageManager.prototype.getMsg = function (msgId) {
- var messages = this.getMsgList()
- var matching = messages.filter(msg => msg.id === msgId)
- return matching.length > 0 ? matching[0] : null
-}
+ // signal update
+ this.emit('update')
+ return msgId
+ }
-MessageManager.prototype.confirmMsg = function (msgId) {
- this._setMsgStatus(msgId, 'confirmed')
-}
+ addMsg (msg) {
+ this.messages.push(msg)
+ this._saveMsgList()
+ }
-MessageManager.prototype.rejectMsg = function (msgId) {
- this._setMsgStatus(msgId, 'rejected')
-}
+ getMsg (msgId) {
+ return this.messages.find(msg => msg.id === msgId)
+ }
-MessageManager.prototype._setMsgStatus = function (msgId, status) {
- var msg = this.getMsg(msgId)
- if (msg) msg.status = status
- this.updateMsg(msg)
-}
+ approveMessage (msgParams) {
+ this.setMsgStatusApproved(msgParams.metamaskId)
+ return this.prepMsgForSigning(msgParams)
+ }
-MessageManager.prototype.updateMsg = function (msg) {
- var messages = this.getMsgList()
- var found, index
- messages.forEach((otherMsg, i) => {
- if (otherMsg.id === msg.id) {
- found = true
- index = i
+ 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('MessageManager - 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
}
- })
- if (found) {
- messages[index] = msg
+ this._saveMsgList()
}
- this._saveMsgList(messages)
+
+ _saveMsgList () {
+ const unapprovedMsgs = this.getUnapprovedMsgs()
+ const unapprovedMsgCount = Object.keys(unapprovedMsgs).length
+ this.memStore.updateState({ unapprovedMsgs, unapprovedMsgCount })
+ this.emit('updateBadge')
+ }
+
}
+function normalizeMsgData(data) {
+ if (data.slice(0, 2) === '0x') {
+ // data is already hex
+ return data
+ } else {
+ // data is unicode, convert to hex
+ return ethUtil.bufferToHex(new Buffer(data, 'utf8'))
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/migrations.js b/app/scripts/lib/migrations.js
deleted file mode 100644
index f026cbe53..000000000
--- a/app/scripts/lib/migrations.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = [
- require('../migrations/002'),
- require('../migrations/003'),
- require('../migrations/004'),
-]
diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js
new file mode 100644
index 000000000..312345263
--- /dev/null
+++ b/app/scripts/lib/migrator/index.js
@@ -0,0 +1,51 @@
+const asyncQ = require('async-q')
+
+class Migrator {
+
+ constructor (opts = {}) {
+ let migrations = opts.migrations || []
+ this.migrations = migrations.sort((a, b) => a.version - b.version)
+ let lastMigration = this.migrations.slice(-1)[0]
+ // use specified defaultVersion or highest migration version
+ this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0
+ }
+
+ // run all pending migrations on meta in place
+ migrateData (versionedData = this.generateInitialState()) {
+ let remaining = this.migrations.filter(migrationIsPending)
+
+ return (
+ asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration))
+ .then(() => versionedData)
+ )
+
+ // migration is "pending" if hit has a higher
+ // version number than currentVersion
+ function migrationIsPending(migration) {
+ return migration.version > versionedData.meta.version
+ }
+ }
+
+ runMigration(versionedData, migration) {
+ return (
+ migration.migrate(versionedData)
+ .then((versionedData) => {
+ if (!versionedData.data) return Promise.reject(new Error('Migrator - Migration returned empty data'))
+ if (migration.version !== undefined && versionedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly'))
+ return Promise.resolve(versionedData)
+ })
+ )
+ }
+
+ generateInitialState (initState) {
+ return {
+ meta: {
+ version: this.defaultVersion,
+ },
+ data: initState,
+ }
+ }
+
+}
+
+module.exports = Migrator
diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js
new file mode 100644
index 000000000..51d89a8fb
--- /dev/null
+++ b/app/scripts/lib/nodeify.js
@@ -0,0 +1,24 @@
+module.exports = function (promiseFn) {
+ return function () {
+ var args = []
+ for (var i = 0; i < arguments.length - 1; i++) {
+ args.push(arguments[i])
+ }
+ var cb = arguments[arguments.length - 1]
+
+ const nodeified = promiseFn.apply(this, args)
+
+ if (!nodeified) {
+ const methodName = String(promiseFn).split('(')[0]
+ throw new Error(`The ${methodName} did not return a Promise, but was nodeified.`)
+ }
+ nodeified.then(function (result) {
+ cb(null, result)
+ })
+ .catch(function (reason) {
+ cb(reason)
+ })
+
+ return nodeified
+ }
+}
diff --git a/app/scripts/lib/notifications.js b/app/scripts/lib/notifications.js
index 6c1601df1..3db1ac6b5 100644
--- a/app/scripts/lib/notifications.js
+++ b/app/scripts/lib/notifications.js
@@ -1,159 +1,65 @@
-const createId = require('hat')
-const extend = require('xtend')
-const unmountComponentAtNode = require('react-dom').unmountComponentAtNode
-const findDOMNode = require('react-dom').findDOMNode
-const render = require('react-dom').render
-const h = require('react-hyperscript')
-const PendingTxDetails = require('../../../ui/app/components/pending-tx-details')
-const PendingMsgDetails = require('../../../ui/app/components/pending-msg-details')
-const MetaMaskUiCss = require('../../../ui/css')
const extension = require('./extension')
-var notificationHandlers = {}
+const height = 520
+const width = 360
const notifications = {
- createUnlockRequestNotification: createUnlockRequestNotification,
- createTxNotification: createTxNotification,
- createMsgNotification: createMsgNotification,
+ show,
+ getPopup,
+ closePopup,
}
module.exports = notifications
window.METAMASK_NOTIFIER = notifications
-setupListeners()
-
-function setupListeners () {
- // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
- if (!extension.notifications) return console.error('Chrome notifications API missing...')
+function show () {
+ getPopup((err, popup) => {
+ if (err) throw err
- // notification button press
- extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) {
- var handlers = notificationHandlers[notificationId]
- if (buttonIndex === 0) {
- handlers.confirm()
+ if (popup) {
+ // bring focus to existing popup
+ extension.windows.update(popup.id, { focused: true })
} else {
- handlers.cancel()
+ // create new popup
+ extension.windows.create({
+ url: 'notification.html',
+ type: 'popup',
+ focused: true,
+ width,
+ height,
+ })
}
- extension.notifications.clear(notificationId)
- })
-
- // notification teardown
- extension.notifications.onClosed.addListener(function (notificationId) {
- delete notificationHandlers[notificationId]
})
}
-// creation helper
-function createUnlockRequestNotification (opts) {
- // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
- if (!extension.notifications) return console.error('Chrome notifications API missing...')
- var message = 'An Ethereum app has requested a signature. Please unlock your account.'
-
- var id = createId()
- extension.notifications.create(id, {
- type: 'basic',
- iconUrl: '/images/icon-128.png',
- title: opts.title,
- message: message,
- })
-}
-
-function createTxNotification (state) {
- // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
- if (!extension.notifications) return console.error('Chrome notifications API missing...')
-
- renderTxNotificationSVG(state, function (err, notificationSvgSource) {
- if (err) throw err
+function getWindows (cb) {
+ // Ignore in test environment
+ if (!extension.windows) {
+ return cb()
+ }
- showNotification(extend(state, {
- title: 'New Unsigned Transaction',
- imageUrl: toSvgUri(notificationSvgSource),
- }))
+ extension.windows.getAll({}, (windows) => {
+ cb(null, windows)
})
}
-function createMsgNotification (state) {
- // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
- if (!extension.notifications) return console.error('Chrome notifications API missing...')
-
- renderMsgNotificationSVG(state, function (err, notificationSvgSource) {
+function getPopup (cb) {
+ getWindows((err, windows) => {
if (err) throw err
-
- showNotification(extend(state, {
- title: 'New Unsigned Message',
- imageUrl: toSvgUri(notificationSvgSource),
- }))
+ cb(null, getPopupIn(windows))
})
}
-function showNotification (state) {
- // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
- if (!extension.notifications) return console.error('Chrome notifications API missing...')
-
- var id = createId()
- extension.notifications.create(id, {
- type: 'image',
- requireInteraction: true,
- iconUrl: '/images/icon-128.png',
- imageUrl: state.imageUrl,
- title: state.title,
- message: '',
- buttons: [{
- title: 'Approve',
- }, {
- title: 'Reject',
- }],
- })
- notificationHandlers[id] = {
- confirm: state.onConfirm,
- cancel: state.onCancel,
- }
-}
-
-function renderTxNotificationSVG (state, cb) {
- var content = h(PendingTxDetails, state)
- renderNotificationSVG(content, cb)
-}
-
-function renderMsgNotificationSVG (state, cb) {
- var content = h(PendingMsgDetails, state)
- renderNotificationSVG(content, cb)
+function getPopupIn (windows) {
+ return windows ? windows.find((win) => {
+ return (win && win.type === 'popup' &&
+ win.height === height &&
+ win.width === width)
+ }) : null
}
-function renderNotificationSVG (content, cb) {
- var container = document.createElement('div')
- var confirmView = h('div.app-primary', {
- style: {
- width: '360px',
- height: '240px',
- padding: '16px',
- // background: '#F7F7F7',
- background: 'white',
- },
- }, [
- h('style', MetaMaskUiCss()),
- content,
- ])
-
- render(confirmView, container, function ready() {
- var rootElement = findDOMNode(this)
- var viewSource = rootElement.outerHTML
- unmountComponentAtNode(container)
- var svgSource = svgWrapper(viewSource)
- // insert content into svg wrapper
- cb(null, svgSource)
+function closePopup () {
+ getPopup((err, popup) => {
+ if (err) throw err
+ if (!popup) return
+ extension.windows.remove(popup.id, console.error)
})
}
-
-function svgWrapper (content) {
- var wrapperSource = `
- <svg xmlns="http://www.w3.org/2000/svg" width="360" height="240">
- <foreignObject x="0" y="0" width="100%" height="100%">
- <body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body>
- </foreignObject>
- </svg>
- `
- return wrapperSource.split('{{content}}').join(content)
-}
-
-function toSvgUri (content) {
- return 'data:image/svg+xml;utf8,' + encodeURIComponent(content)
-}
diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js
index f54ff7653..bd114c394 100644
--- a/app/scripts/lib/obj-multiplex.js
+++ b/app/scripts/lib/obj-multiplex.js
@@ -10,9 +10,9 @@ function ObjectMultiplex (opts) {
var data = chunk.data
var substream = mx.streams[name]
if (!substream) {
- console.warn('orphaned data for stream ' + name)
+ console.warn(`orphaned data for stream "${name}"`)
} else {
- substream.push(data)
+ if (substream.push) substream.push(data)
}
return cb()
})
@@ -36,5 +36,9 @@ function ObjectMultiplex (opts) {
}
return substream
}
+ // ignore streams (dont display orphaned data warning)
+ mx.ignoreStream = function (name) {
+ mx.streams[name] = true
+ }
return mx
}
diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js
index 1889e3c04..607a9c9ed 100644
--- a/app/scripts/lib/port-stream.js
+++ b/app/scripts/lib/port-stream.js
@@ -30,8 +30,7 @@ PortDuplexStream.prototype._onMessage = function (msg) {
PortDuplexStream.prototype._onDisconnect = function () {
try {
- // this.end()
- this.emit('close')
+ this.push(null)
} catch (err) {
this.emit('error', err)
}
@@ -52,12 +51,11 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
// console.log('PortDuplexStream - sent message', msg)
this._port.postMessage(msg)
}
- cb()
} catch (err) {
- console.error(err)
- // this.emit('error', err)
- cb(new Error('PortDuplexStream - disconnected'))
+ // console.error(err)
+ return cb(new Error('PortDuplexStream - disconnected'))
}
+ cb()
}
// util
diff --git a/app/scripts/lib/random-id.js b/app/scripts/lib/random-id.js
new file mode 100644
index 000000000..788f3370f
--- /dev/null
+++ b/app/scripts/lib/random-id.js
@@ -0,0 +1,9 @@
+const MAX = Number.MAX_SAFE_INTEGER
+
+let idCounter = Math.round(Math.random() * MAX)
+function createRandomId () {
+ idCounter = idCounter % MAX
+ return idCounter++
+}
+
+module.exports = createRandomId
diff --git a/app/scripts/lib/remote-store.js b/app/scripts/lib/remote-store.js
deleted file mode 100644
index fbfab7bad..000000000
--- a/app/scripts/lib/remote-store.js
+++ /dev/null
@@ -1,97 +0,0 @@
-const Dnode = require('dnode')
-const inherits = require('util').inherits
-
-module.exports = {
- HostStore: HostStore,
- RemoteStore: RemoteStore,
-}
-
-function BaseStore (initState) {
- this._state = initState || {}
- this._subs = []
-}
-
-BaseStore.prototype.set = function (key, value) {
- throw Error('Not implemented.')
-}
-
-BaseStore.prototype.get = function (key) {
- return this._state[key]
-}
-
-BaseStore.prototype.subscribe = function (fn) {
- this._subs.push(fn)
- var unsubscribe = this.unsubscribe.bind(this, fn)
- return unsubscribe
-}
-
-BaseStore.prototype.unsubscribe = function (fn) {
- var index = this._subs.indexOf(fn)
- if (index !== -1) this._subs.splice(index, 1)
-}
-
-BaseStore.prototype._emitUpdates = function (state) {
- this._subs.forEach(function (handler) {
- handler(state)
- })
-}
-
-//
-// host
-//
-
-inherits(HostStore, BaseStore)
-function HostStore (initState, opts) {
- BaseStore.call(this, initState)
-}
-
-HostStore.prototype.set = function (key, value) {
- this._state[key] = value
- process.nextTick(this._emitUpdates.bind(this, this._state))
-}
-
-HostStore.prototype.createStream = function () {
- var dnode = Dnode({
- // update: this._didUpdate.bind(this),
- })
- dnode.on('remote', this._didConnect.bind(this))
- return dnode
-}
-
-HostStore.prototype._didConnect = function (remote) {
- this.subscribe(function (state) {
- remote.update(state)
- })
- remote.update(this._state)
-}
-
-//
-// remote
-//
-
-inherits(RemoteStore, BaseStore)
-function RemoteStore (initState, opts) {
- BaseStore.call(this, initState)
- this._remote = null
-}
-
-RemoteStore.prototype.set = function (key, value) {
- this._remote.set(key, value)
-}
-
-RemoteStore.prototype.createStream = function () {
- var dnode = Dnode({
- update: this._didUpdate.bind(this),
- })
- dnode.once('remote', this._didConnect.bind(this))
- return dnode
-}
-
-RemoteStore.prototype._didConnect = function (remote) {
- this._remote = remote
-}
-
-RemoteStore.prototype._didUpdate = function (state) {
- this._state = state
- this._emitUpdates(state)
-}
diff --git a/app/scripts/lib/sig-util.js b/app/scripts/lib/sig-util.js
new file mode 100644
index 000000000..193dda381
--- /dev/null
+++ b/app/scripts/lib/sig-util.js
@@ -0,0 +1,28 @@
+const ethUtil = require('ethereumjs-util')
+
+module.exports = {
+
+ concatSig: function (v, r, s) {
+ const rSig = ethUtil.fromSigned(r)
+ const sSig = ethUtil.fromSigned(s)
+ const vSig = ethUtil.bufferToInt(v)
+ const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64)
+ const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64)
+ const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig))
+ return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex')
+ },
+
+ normalize: function (address) {
+ if (!address) return
+ return ethUtil.addHexPrefix(address.toLowerCase())
+ },
+
+}
+
+function padWithZeroes (number, length) {
+ var myString = '' + number
+ while (myString.length < length) {
+ myString = '0' + myString
+ }
+ return myString
+}
diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js
index 1b7b89d14..ba79990cc 100644
--- a/app/scripts/lib/stream-utils.js
+++ b/app/scripts/lib/stream-utils.js
@@ -1,4 +1,5 @@
const Through = require('through2')
+const endOfStream = require('end-of-stream')
const ObjectMultiplex = require('./obj-multiplex')
module.exports = {
@@ -24,11 +25,11 @@ function jsonStringifyStream () {
function setupMultiplex (connectionStream) {
var mx = ObjectMultiplex()
connectionStream.pipe(mx).pipe(connectionStream)
- mx.on('error', function (err) {
- console.error(err)
+ endOfStream(mx, function (err) {
+ if (err) console.error(err)
})
- connectionStream.on('error', function (err) {
- console.error(err)
+ endOfStream(connectionStream, function (err) {
+ if (err) console.error(err)
mx.destroy()
})
return mx
diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js
new file mode 100644
index 000000000..5116cb93b
--- /dev/null
+++ b/app/scripts/lib/tx-utils.js
@@ -0,0 +1,132 @@
+const async = require('async')
+const EthQuery = require('eth-query')
+const ethUtil = require('ethereumjs-util')
+const Transaction = require('ethereumjs-tx')
+const normalize = require('./sig-util').normalize
+const BN = ethUtil.BN
+
+/*
+tx-utils are utility methods for Transaction manager
+its passed a provider and that is passed to ethquery
+and used to do things like calculate gas of a tx.
+*/
+
+module.exports = class txProviderUtils {
+ constructor (provider) {
+ this.provider = provider
+ this.query = new EthQuery(provider)
+ }
+
+ analyzeGasUsage (txData, cb) {
+ var self = this
+ this.query.getBlockByNumber('latest', true, (err, block) => {
+ if (err) return cb(err)
+ async.waterfall([
+ self.estimateTxGas.bind(self, txData, block.gasLimit),
+ self.setTxGas.bind(self, txData, block.gasLimit),
+ ], cb)
+ })
+ }
+
+ estimateTxGas (txData, blockGasLimitHex, cb) {
+ const txParams = txData.txParams
+ // check if gasLimit is already specified
+ txData.gasLimitSpecified = Boolean(txParams.gas)
+ // if not, fallback to block gasLimit
+ if (!txData.gasLimitSpecified) {
+ txParams.gas = blockGasLimitHex
+ }
+ // run tx, see if it will OOG
+ this.query.estimateGas(txParams, cb)
+ }
+
+ setTxGas (txData, blockGasLimitHex, estimatedGasHex, cb) {
+ txData.estimatedGas = estimatedGasHex
+ const txParams = txData.txParams
+
+ // if gasLimit was specified and doesnt OOG,
+ // use original specified amount
+ if (txData.gasLimitSpecified) {
+ txData.estimatedGas = txParams.gas
+ cb()
+ return
+ }
+ // if gasLimit not originally specified,
+ // try adding an additional gas buffer to our estimation for safety
+ const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16)
+ const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16)
+ const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16)
+ // added gas buffer is too high
+ if (estimationWithBuffer.gt(blockGasLimitBn)) {
+ txParams.gas = txData.estimatedGas
+ // added gas buffer is safe
+ } else {
+ const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
+ txParams.gas = gasWithBufferHex
+ }
+ cb()
+ return
+ }
+
+ addGasBuffer (gas) {
+ const gasBuffer = new BN('100000', 10)
+ const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
+ const correct = bnGas.add(gasBuffer)
+ return ethUtil.addHexPrefix(correct.toString(16))
+ }
+
+ fillInTxParams (txParams, cb) {
+ let fromAddress = txParams.from
+ let reqs = {}
+
+ if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb)
+ if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb)
+ if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb)
+
+ async.parallel(reqs, function(err, result) {
+ if (err) return cb(err)
+ // write results to txParams obj
+ Object.assign(txParams, result)
+ cb()
+ })
+ }
+
+ // builds ethTx from txParams object
+ buildEthTxFromParams (txParams, gasMultiplier = 1) {
+ // apply gas multiplyer
+ let gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
+ // multiply and divide by 100 so as to add percision to integer mul
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
+ txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
+ // normalize values
+ txParams.to = normalize(txParams.to)
+ txParams.from = normalize(txParams.from)
+ txParams.value = normalize(txParams.value)
+ txParams.data = normalize(txParams.data)
+ txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
+ txParams.nonce = normalize(txParams.nonce)
+ // build ethTx
+ const ethTx = new Transaction(txParams)
+ return ethTx
+ }
+
+ publishTransaction (rawTx, cb) {
+ this.query.sendRawTransaction(rawTx, cb)
+ }
+
+ validateTxParams (txParams, cb) {
+ if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
+ cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`))
+ } else {
+ cb()
+ }
+ }
+
+
+}
+
+// util
+
+function isUndef(value) {
+ return value === undefined
+}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index d53094e43..29b13dc62 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1,303 +1,595 @@
+const EventEmitter = require('events')
const extend = require('xtend')
-const EthStore = require('eth-store')
+const promiseToCallback = require('promise-to-callback')
+const pipe = require('pump')
+const Dnode = require('dnode')
+const ObservableStore = require('obs-store')
+const storeTransform = require('obs-store/lib/transform')
+const EthStore = require('./lib/eth-store')
+const EthQuery = require('eth-query')
+const streamIntoProvider = require('web3-stream-provider/handler')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
-const IdentityStore = require('./lib/idStore')
-const messageManager = require('./lib/message-manager')
-const HostStore = require('./lib/remote-store.js').HostStore
-const Web3 = require('web3')
+const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
+const KeyringController = require('./keyring-controller')
+const PreferencesController = require('./lib/controllers/preferences')
+const CurrencyController = require('./lib/controllers/currency')
+const NoticeController = require('./notice-controller')
+const ShapeShiftController = require('./lib/controllers/shapeshift')
+const MessageManager = require('./lib/message-manager')
+const TxManager = require('./transaction-manager')
const ConfigManager = require('./lib/config-manager')
const extension = require('./lib/extension')
+const autoFaucet = require('./lib/auto-faucet')
+const nodeify = require('./lib/nodeify')
+const IdStoreMigrator = require('./lib/idStore-migrator')
+const accountImporter = require('./account-import-strategies')
-module.exports = class MetamaskController {
+const version = require('../manifest.json').version
+
+module.exports = class MetamaskController extends EventEmitter {
constructor (opts) {
+ super()
this.opts = opts
- this.configManager = new ConfigManager(opts)
- this.idStore = new IdentityStore({
- configManager: this.configManager,
+ let initState = opts.initState || {}
+
+ // observable state store
+ this.store = new ObservableStore(initState)
+
+ // network store
+ this.networkStore = new ObservableStore({ network: 'loading' })
+
+ // config manager
+ this.configManager = new ConfigManager({
+ store: this.store,
+ })
+
+ // preferences controller
+ this.preferencesController = new PreferencesController({
+ initState: initState.PreferencesController,
+ })
+
+ // currency controller
+ this.currencyController = new CurrencyController({
+ initState: initState.CurrencyController,
+ })
+ this.currencyController.updateConversionRate()
+ this.currencyController.scheduleConversionInterval()
+
+ // rpc provider
+ this.provider = this.initializeProvider()
+ this.provider.on('block', this.logBlock.bind(this))
+ this.provider.on('error', this.verifyNetwork.bind(this))
+
+ // eth data query tools
+ this.ethQuery = new EthQuery(this.provider)
+ this.ethStore = new EthStore({
+ provider: this.provider,
+ blockTracker: this.provider,
+ })
+
+ // key mgmt
+ this.keyringController = new KeyringController({
+ initState: initState.KeyringController,
+ ethStore: this.ethStore,
+ getNetwork: this.getNetworkState.bind(this),
+ })
+ this.keyringController.on('newAccount', (address) => {
+ this.preferencesController.setSelectedAddress(address)
+ autoFaucet(address)
+ })
+
+ // tx mgmt
+ this.txManager = new TxManager({
+ initState: initState.TransactionManager,
+ networkStore: this.networkStore,
+ preferencesStore: this.preferencesController.store,
+ txHistoryLimit: 40,
+ getNetwork: this.getNetworkState.bind(this),
+ signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
+ provider: this.provider,
+ blockTracker: this.provider,
+ })
+
+ // notices
+ this.noticeController = new NoticeController({
+ initState: initState.NoticeController,
})
- this.provider = this.initializeProvider(opts)
- this.ethStore = new EthStore(this.provider)
- this.idStore.setStore(this.ethStore)
- this.messageManager = messageManager
+ this.noticeController.updateNoticesList()
+ // to be uncommented when retrieving notices from a remote server.
+ // this.noticeController.startPolling()
+
+ this.shapeshiftController = new ShapeShiftController({
+ initState: initState.ShapeShiftController,
+ })
+
+ this.lookupNetwork()
+ this.messageManager = new MessageManager()
this.publicConfigStore = this.initPublicConfigStore()
- this.configManager.setCurrentFiat('USD')
- this.configManager.updateConversionRate()
- this.scheduleConversionInterval()
+
+ // TEMPORARY UNTIL FULL DEPRECATION:
+ this.idStoreMigrator = new IdStoreMigrator({
+ configManager: this.configManager,
+ })
+
+ // manual disk state subscriptions
+ this.txManager.store.subscribe((state) => {
+ this.store.updateState({ TransactionManager: state })
+ })
+ this.keyringController.store.subscribe((state) => {
+ this.store.updateState({ KeyringController: state })
+ })
+ this.preferencesController.store.subscribe((state) => {
+ this.store.updateState({ PreferencesController: state })
+ })
+ this.currencyController.store.subscribe((state) => {
+ this.store.updateState({ CurrencyController: state })
+ })
+ this.noticeController.store.subscribe((state) => {
+ this.store.updateState({ NoticeController: state })
+ })
+ this.shapeshiftController.store.subscribe((state) => {
+ this.store.updateState({ ShapeShiftController: state })
+ })
+
+ // manual mem state subscriptions
+ this.networkStore.subscribe(this.sendUpdate.bind(this))
+ this.ethStore.subscribe(this.sendUpdate.bind(this))
+ this.txManager.memStore.subscribe(this.sendUpdate.bind(this))
+ this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
+ this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
+ this.preferencesController.store.subscribe(this.sendUpdate.bind(this))
+ this.currencyController.store.subscribe(this.sendUpdate.bind(this))
+ this.noticeController.memStore.subscribe(this.sendUpdate.bind(this))
+ this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this))
}
+ //
+ // Constructor helpers
+ //
+
+ initializeProvider () {
+ let provider = MetaMaskProvider({
+ static: {
+ eth_syncing: false,
+ web3_clientVersion: `MetaMask/v${version}`,
+ },
+ rpcUrl: this.configManager.getCurrentRpcAddress(),
+ // account mgmt
+ getAccounts: (cb) => {
+ let selectedAddress = this.preferencesController.getSelectedAddress()
+ let result = selectedAddress ? [selectedAddress] : []
+ cb(null, result)
+ },
+ // tx signing
+ processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb),
+ // msg signing
+ processMessage: this.newUnsignedMessage.bind(this),
+ })
+ return provider
+ }
+
+ initPublicConfigStore () {
+ // get init state
+ const publicConfigStore = new ObservableStore()
+
+ // sync publicConfigStore with transform
+ pipe(
+ this.store,
+ storeTransform(selectPublicState.bind(this)),
+ publicConfigStore
+ )
+
+ function selectPublicState(state) {
+ const result = { selectedAddress: undefined }
+ try {
+ result.selectedAddress = state.PreferencesController.selectedAddress
+ result.networkVersion = this.getNetworkState()
+ } catch (_) {}
+ return result
+ }
+
+ return publicConfigStore
+ }
+
+ //
+ // State Management
+ //
+
getState () {
+
+ const wallet = this.configManager.getWallet()
+ const vault = this.keyringController.store.getState().vault
+ const isInitialized = (!!wallet || !!vault)
return extend(
+ {
+ isInitialized,
+ },
+ this.networkStore.getState(),
this.ethStore.getState(),
- this.idStore.getState(),
- this.configManager.getConfig()
+ this.txManager.memStore.getState(),
+ this.messageManager.memStore.getState(),
+ this.keyringController.memStore.getState(),
+ this.preferencesController.store.getState(),
+ this.currencyController.store.getState(),
+ this.noticeController.memStore.getState(),
+ // config manager
+ this.configManager.getConfig(),
+ this.shapeshiftController.store.getState(),
+ {
+ lostAccounts: this.configManager.getLostAccounts(),
+ seedWords: this.configManager.getSeedWords(),
+ }
)
}
+ //
+ // Remote Features
+ //
+
getApi () {
- const idStore = this.idStore
+ const keyringController = this.keyringController
+ const preferencesController = this.preferencesController
+ const txManager = this.txManager
+ const messageManager = this.messageManager
+ const noticeController = this.noticeController
return {
- getState: (cb) => { cb(null, this.getState()) },
- setRpcTarget: this.setRpcTarget.bind(this),
- setProviderType: this.setProviderType.bind(this),
- useEtherscanProvider: this.useEtherscanProvider.bind(this),
- agreeToDisclaimer: this.agreeToDisclaimer.bind(this),
- setCurrentFiat: this.setCurrentFiat.bind(this),
- agreeToEthWarning: this.agreeToEthWarning.bind(this),
-
- // forward directly to idStore
- createNewVault: idStore.createNewVault.bind(idStore),
- recoverFromSeed: idStore.recoverFromSeed.bind(idStore),
- submitPassword: idStore.submitPassword.bind(idStore),
- setSelectedAddress: idStore.setSelectedAddress.bind(idStore),
- approveTransaction: idStore.approveTransaction.bind(idStore),
- cancelTransaction: idStore.cancelTransaction.bind(idStore),
- signMessage: idStore.signMessage.bind(idStore),
- cancelMessage: idStore.cancelMessage.bind(idStore),
- setLocked: idStore.setLocked.bind(idStore),
- clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore),
- exportAccount: idStore.exportAccount.bind(idStore),
- revealAccount: idStore.revealAccount.bind(idStore),
- saveAccountLabel: idStore.saveAccountLabel.bind(idStore),
- tryPassword: idStore.tryPassword.bind(idStore),
- recoverSeed: idStore.recoverSeed.bind(idStore),
+ // etc
+ getState: (cb) => cb(null, this.getState()),
+ setRpcTarget: this.setRpcTarget.bind(this),
+ setProviderType: this.setProviderType.bind(this),
+ useEtherscanProvider: this.useEtherscanProvider.bind(this),
+ setCurrentCurrency: this.setCurrentCurrency.bind(this),
+ setGasMultiplier: this.setGasMultiplier.bind(this),
+ markAccountsFound: this.markAccountsFound.bind(this),
// coinbase
buyEth: this.buyEth.bind(this),
// shapeshift
createShapeShiftTx: this.createShapeShiftTx.bind(this),
+
+ // primary HD keyring management
+ addNewAccount: this.addNewAccount.bind(this),
+ placeSeedWords: this.placeSeedWords.bind(this),
+ clearSeedWordCache: this.clearSeedWordCache.bind(this),
+ importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
+
+ // vault management
+ submitPassword: this.submitPassword.bind(this),
+
+ // PreferencesController
+ setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
+
+ // KeyringController
+ setLocked: nodeify(keyringController.setLocked).bind(keyringController),
+ createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
+ createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController),
+ addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
+ saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
+ exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
+
+ // txManager
+ approveTransaction: txManager.approveTransaction.bind(txManager),
+ cancelTransaction: txManager.cancelTransaction.bind(txManager),
+
+ // messageManager
+ signMessage: this.signMessage.bind(this),
+ cancelMessage: messageManager.rejectMsg.bind(messageManager),
+
+ // notices
+ checkNotices: noticeController.updateNoticesList.bind(noticeController),
+ markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
}
}
- setupProviderConnection (stream, originDomain) {
- stream.on('data', this.onRpcRequest.bind(this, stream, originDomain))
+ setupUntrustedCommunication (connectionStream, originDomain) {
+ // setup multiplexing
+ var mx = setupMultiplex(connectionStream)
+ // connect features
+ this.setupProviderConnection(mx.createStream('provider'), originDomain)
+ this.setupPublicConfig(mx.createStream('publicConfig'))
}
- onRpcRequest (stream, originDomain, request) {
- var payloads = Array.isArray(request) ? request : [request]
- payloads.forEach(function (payload) {
- // Append origin to rpc payload
- payload.origin = originDomain
- // Append origin to signature request
- if (payload.method === 'eth_sendTransaction') {
- payload.params[0].origin = originDomain
- } else if (payload.method === 'eth_sign') {
- payload.params.push({ origin: originDomain })
- }
- })
+ setupTrustedCommunication (connectionStream, originDomain) {
+ // setup multiplexing
+ var mx = setupMultiplex(connectionStream)
+ // connect features
+ this.setupControllerConnection(mx.createStream('controller'))
+ this.setupProviderConnection(mx.createStream('provider'), originDomain)
+ }
- // handle rpc request
- this.provider.sendAsync(request, function onPayloadHandled (err, response) {
- logger(err, request, response)
- if (response) {
- try {
- stream.write(response)
- } catch (err) {
- logger(err)
- }
- }
+ setupControllerConnection (outStream) {
+ const api = this.getApi()
+ const dnode = Dnode(api)
+ outStream.pipe(dnode).pipe(outStream)
+ dnode.on('remote', (remote) => {
+ // push updates to popup
+ const sendUpdate = remote.sendUpdate.bind(remote)
+ this.on('update', sendUpdate)
})
+ }
+ setupProviderConnection (outStream, originDomain) {
+ streamIntoProvider(outStream, this.provider, logger)
function logger (err, request, response) {
if (err) return console.error(err)
- if (!request.isMetamaskInternal) {
- if (global.METAMASK_DEBUG) {
- console.log(`RPC (${originDomain}):`, request, '->', response)
- }
- if (response.error) {
- console.error('Error in RPC response:\n', response.error)
- }
+ if (response.error) {
+ console.error('Error in RPC response:\n', response.error)
+ }
+ if (request.isMetamaskInternal) return
+ if (global.METAMASK_DEBUG) {
+ console.log(`RPC (${originDomain}):`, request, '->', response)
}
}
}
- sendUpdate () {
- if (this.remote) {
- this.remote.sendUpdate(this.getState())
- }
+ setupPublicConfig (outStream) {
+ pipe(
+ this.publicConfigStore,
+ outStream
+ )
}
- initializeProvider (opts) {
- const idStore = this.idStore
+ sendUpdate () {
+ this.emit('update', this.getState())
+ }
- var providerOpts = {
- rpcUrl: this.configManager.getCurrentRpcAddress(),
- // account mgmt
- getAccounts: (cb) => {
- var selectedAddress = idStore.getSelectedAddress()
- var result = selectedAddress ? [selectedAddress] : []
- cb(null, result)
- },
- // tx signing
- approveTransaction: this.newUnsignedTransaction.bind(this),
- signTransaction: idStore.signTransaction.bind(idStore),
- // msg signing
- approveMessage: this.newUnsignedMessage.bind(this),
- signMessage: idStore.signMessage.bind(idStore),
- }
+ //
+ // Vault Management
+ //
- var provider = MetaMaskProvider(providerOpts)
- var web3 = new Web3(provider)
- idStore.web3 = web3
- idStore.getNetwork()
+ submitPassword (password, cb) {
+ this.migrateOldVaultIfAny(password)
+ .then(this.keyringController.submitPassword.bind(this.keyringController, password))
+ .then((newState) => { cb(null, newState) })
+ .catch((reason) => { cb(reason) })
+ }
- provider.on('block', this.processBlock.bind(this))
- provider.on('error', idStore.getNetwork.bind(idStore))
+ //
+ // Opinionated Keyring Management
+ //
- return provider
+ 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)
}
- initPublicConfigStore () {
- // get init state
- var initPublicState = extend(
- idStoreToPublic(this.idStore.getState()),
- configToPublic(this.configManager.getConfig())
- )
+ // Adds the current vault's seed words to the UI's state tree.
+ //
+ // Used when creating a first vault, to allow confirmation.
+ // Also used when revealing the seed words in the confirmation view.
+ placeSeedWords (cb) {
+ const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0]
+ if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found'))
+ primaryKeyring.serialize()
+ .then((serialized) => {
+ const seedWords = serialized.mnemonic
+ this.configManager.setSeedWords(seedWords)
+ cb()
+ })
+ }
- var publicConfigStore = new HostStore(initPublicState)
+ // ClearSeedWordCache
+ //
+ // Removes the primary account's seed words from the UI's state tree,
+ // ensuring they are only ever available in the background process.
+ clearSeedWordCache (cb) {
+ this.configManager.setSeedWords(null)
+ cb(null, this.preferencesController.getSelectedAddress())
+ }
- // subscribe to changes
- this.configManager.subscribe(function (state) {
- storeSetFromObj(publicConfigStore, configToPublic(state))
- })
- this.idStore.on('update', function (state) {
- storeSetFromObj(publicConfigStore, idStoreToPublic(state))
+ importAccountWithStrategy (strategy, args, cb) {
+ accountImporter.importAccount(strategy, args)
+ .then((privateKey) => {
+ return this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ])
})
+ .then(keyring => keyring.getAccounts())
+ .then((accounts) => this.preferencesController.setSelectedAddress(accounts[0]))
+ .then(() => { cb(null, this.keyringController.fullUpdate()) })
+ .catch((reason) => { cb(reason) })
+ }
- // idStore substate
- function idStoreToPublic (state) {
- return {
- selectedAddress: state.selectedAddress,
- }
- }
- // config substate
- function configToPublic (state) {
- return {
- provider: state.provider,
- selectedAddress: state.selectedAccount,
- }
- }
- // dump obj into store
- function storeSetFromObj (store, obj) {
- Object.keys(obj).forEach(function (key) {
- store.set(key, obj[key])
+
+ //
+ // Identity Management
+ //
+
+ newUnapprovedTransaction (txParams, cb) {
+ const self = this
+ self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => {
+ if (err) return cb(err)
+ self.sendUpdate()
+ self.opts.showUnapprovedTx(txMeta)
+ // listen for tx completion (success, fail)
+ self.txManager.once(`${txMeta.id}:finished`, (status) => {
+ switch (status) {
+ case 'submitted':
+ return cb(null, txMeta.hash)
+ case 'rejected':
+ return cb(new Error('MetaMask Tx Signature: User denied transaction signature.'))
+ default:
+ return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(txMeta.txParams)}`))
+ }
})
- }
+ })
+ }
- return publicConfigStore
+ newUnsignedMessage (msgParams, cb) {
+ let msgId = this.messageManager.addUnapprovedMessage(msgParams)
+ this.sendUpdate()
+ this.opts.showUnconfirmedMessage()
+ this.messageManager.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 transaction signature.'))
+ default:
+ return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
+ }
+ })
}
- newUnsignedTransaction (txParams, onTxDoneCb) {
- const idStore = this.idStore
- var state = idStore.getState()
+ signMessage (msgParams, cb) {
+ const msgId = msgParams.metamaskId
+ promiseToCallback(
+ // sets the status op the message to 'approved'
+ // and removes the metamaskId for signing
+ this.messageManager.approveMessage(msgParams)
+ .then((cleanMsgParams) => {
+ // signs the message
+ return this.keyringController.signMessage(cleanMsgParams)
+ })
+ .then((rawSig) => {
+ // tells the listener that the message has been signed
+ // and can be returned to the dapp
+ this.messageManager.setMsgStatusSigned(msgId, rawSig)
+ })
+ )(cb)
+ }
- // It's locked
- if (!state.isUnlocked) {
- this.opts.unlockAccountMessage()
- idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, noop)
- // It's unlocked
- } else {
- idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => {
- if (err) return onTxDoneCb(err)
- this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb)
- })
- }
+ markAccountsFound (cb) {
+ this.configManager.setLostAccounts([])
+ this.sendUpdate()
+ cb(null, this.getState())
}
- newUnsignedMessage (msgParams, cb) {
- var state = this.idStore.getState()
- if (!state.isUnlocked) {
- this.idStore.addUnconfirmedMessage(msgParams, cb)
- this.opts.unlockAccountMessage()
- } else {
- this.addUnconfirmedMessage(msgParams, cb)
+ // Migrate Old Vault If Any
+ // @string password
+ //
+ // returns Promise()
+ //
+ // Temporary step used when logging in.
+ // Checks if old style (pre-3.0.0) Metamask Vault exists.
+ // If so, persists that vault in the new vault format
+ // with the provided password, so the other unlock steps
+ // may be completed without interruption.
+ migrateOldVaultIfAny (password) {
+
+ if (!this.checkIfShouldMigrate()) {
+ return Promise.resolve(password)
}
+
+ const keyringController = this.keyringController
+
+ return this.idStoreMigrator.migratedVaultForPassword(password)
+ .then(this.restoreOldVaultAccounts.bind(this))
+ .then(this.restoreOldLostAccounts.bind(this))
+ .then(keyringController.persistAllKeyrings.bind(keyringController, password))
+ .then(() => password)
}
- addUnconfirmedMessage (msgParams, cb) {
- const idStore = this.idStore
- const msgId = idStore.addUnconfirmedMessage(msgParams, cb)
- this.opts.showUnconfirmedMessage(msgParams, msgId)
+ checkIfShouldMigrate() {
+ return !!this.configManager.getWallet() && !this.configManager.getVault()
}
- setupPublicConfig (stream) {
- var storeStream = this.publicConfigStore.createStream()
- stream.pipe(storeStream).pipe(stream)
+ restoreOldVaultAccounts(migratorOutput) {
+ const { serialized } = migratorOutput
+ return this.keyringController.restoreKeyring(serialized)
+ .then(() => migratorOutput)
}
- // Log blocks
- processBlock (block) {
- if (global.METAMASK_DEBUG) {
- console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
+ restoreOldLostAccounts(migratorOutput) {
+ const { lostAccounts } = migratorOutput
+ if (lostAccounts) {
+ this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
+ return this.importLostAccounts(migratorOutput)
}
- this.verifyNetwork()
+ return Promise.resolve(migratorOutput)
}
- verifyNetwork () {
- // Check network when restoring connectivity:
- if (this.idStore._currentState.network === 'loading') {
- this.idStore.getNetwork()
- }
+ // IMPORT LOST ACCOUNTS
+ // @Object with key lostAccounts: @Array accounts <{ address, privateKey }>
+ // Uses the array's private keys to create a new Simple Key Pair keychain
+ // and add it to the keyring controller.
+ importLostAccounts ({ lostAccounts }) {
+ const privKeys = lostAccounts.map(acct => acct.privateKey)
+ return this.keyringController.restoreKeyring({
+ type: 'Simple Key Pair',
+ data: privKeys,
+ })
}
+ //
// config
//
- agreeToDisclaimer (cb) {
- try {
- this.configManager.setConfirmed(true)
- cb()
- } catch (e) {
- cb(e)
+ // Log blocks
+ logBlock (block) {
+ if (global.METAMASK_DEBUG) {
+ console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
}
+ this.verifyNetwork()
}
- setCurrentFiat (fiat, cb) {
+ setCurrentCurrency (currencyCode, cb) {
try {
- this.configManager.setCurrentFiat(fiat)
- this.configManager.updateConversionRate()
- this.scheduleConversionInterval()
+ this.currencyController.setCurrentCurrency(currencyCode)
+ this.currencyController.updateConversionRate()
const data = {
- conversionRate: this.configManager.getConversionRate(),
- currentFiat: this.configManager.getCurrentFiat(),
- conversionDate: this.configManager.getConversionDate(),
+ conversionRate: this.currencyController.getConversionRate(),
+ currentFiat: this.currencyController.getCurrentCurrency(),
+ conversionDate: this.currencyController.getConversionDate(),
}
- cb(data)
- } catch (e) {
- cb(null, e)
+ cb(null, data)
+ } catch (err) {
+ cb(err)
}
}
- scheduleConversionInterval () {
- if (this.conversionInterval) {
- clearInterval(this.conversionInterval)
+ buyEth (address, amount) {
+ if (!amount) amount = '5'
+
+ const network = this.getNetworkState()
+ let url
+
+ switch (network) {
+ case '1':
+ url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
+ break
+
+ case '3':
+ url = 'https://faucet.metamask.io/'
+ break
}
- this.conversionInterval = setInterval(() => {
- this.configManager.updateConversionRate()
- }, 300000)
+
+ if (url) extension.tabs.create({ url })
}
- agreeToEthWarning (cb) {
+ createShapeShiftTx (depositAddress, depositType) {
+ this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
+ }
+
+ setGasMultiplier (gasMultiplier, cb) {
try {
- this.configManager.setShouldntShowWarning()
+ this.txManager.setGasMultiplier(gasMultiplier)
cb()
- } catch (e) {
- cb(e)
+ } catch (err) {
+ cb(err)
}
}
- // called from popup
+ //
+ // network
+ //
+
+ verifyNetwork () {
+ // Check network when restoring connectivity:
+ if (this.isNetworkLoading()) this.lookupNetwork()
+ }
+
setRpcTarget (rpcTarget) {
this.configManager.setRpcTarget(rpcTarget)
extension.runtime.reload()
- this.idStore.getNetwork()
+ this.lookupNetwork()
}
setProviderType (type) {
this.configManager.setProviderType(type)
extension.runtime.reload()
- this.idStore.getNetwork()
+ this.lookupNetwork()
}
useEtherscanProvider () {
@@ -305,24 +597,33 @@ module.exports = class MetamaskController {
extension.runtime.reload()
}
- buyEth (address, amount) {
- if (!amount) amount = '5'
+ getNetworkState () {
+ return this.networkStore.getState().network
+ }
- var network = this.idStore._currentState.network
- var url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
+ setNetworkState (network) {
+ return this.networkStore.updateState({ network })
+ }
+
+ isNetworkLoading () {
+ return this.getNetworkState() === 'loading'
+ }
- if (network === '2') {
- url = 'https://testfaucet.metamask.io/'
+ lookupNetwork (err) {
+ if (err) {
+ this.setNetworkState('loading')
}
- extension.tabs.create({
- url,
+ this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
+ if (err) {
+ this.setNetworkState('loading')
+ return
+ }
+ if (global.METAMASK_DEBUG) {
+ console.log('web3.getNetwork returned ' + network)
+ }
+ this.setNetworkState(network)
})
}
- createShapeShiftTx (depositAddress, depositType) {
- this.configManager.createShapeShiftTx(depositAddress, depositType)
- }
}
-
-function noop () {}
diff --git a/app/scripts/migrations/002.js b/app/scripts/migrations/002.js
index 0b654f825..36a870342 100644
--- a/app/scripts/migrations/002.js
+++ b/app/scripts/migrations/002.js
@@ -1,13 +1,20 @@
+const version = 2
+
+const clone = require('clone')
+
+
module.exports = {
- version: 2,
+ version,
- migrate: function (data) {
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
try {
- if (data.config.provider.type === 'etherscan') {
- data.config.provider.type = 'rpc'
- data.config.provider.rpcTarget = 'https://rpc.metamask.io/'
+ if (versionedData.data.config.provider.type === 'etherscan') {
+ versionedData.data.config.provider.type = 'rpc'
+ versionedData.data.config.provider.rpcTarget = 'https://rpc.metamask.io/'
}
} catch (e) {}
- return data
+ return Promise.resolve(versionedData)
},
}
diff --git a/app/scripts/migrations/003.js b/app/scripts/migrations/003.js
index 617c55c09..1893576ad 100644
--- a/app/scripts/migrations/003.js
+++ b/app/scripts/migrations/003.js
@@ -1,15 +1,20 @@
-var oldTestRpc = 'https://rawtestrpc.metamask.io/'
-var newTestRpc = 'https://testrpc.metamask.io/'
+const version = 3
+const oldTestRpc = 'https://rawtestrpc.metamask.io/'
+const newTestRpc = 'https://testrpc.metamask.io/'
+
+const clone = require('clone')
module.exports = {
- version: 3,
+ version,
- migrate: function (data) {
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
try {
- if (data.config.provider.rpcTarget === oldTestRpc) {
- data.config.provider.rpcTarget = newTestRpc
+ if (versionedData.data.config.provider.rpcTarget === oldTestRpc) {
+ versionedData.data.config.provider.rpcTarget = newTestRpc
}
} catch (e) {}
- return data
+ return Promise.resolve(versionedData)
},
}
diff --git a/app/scripts/migrations/004.js b/app/scripts/migrations/004.js
index 1329a1eed..405d932f8 100644
--- a/app/scripts/migrations/004.js
+++ b/app/scripts/migrations/004.js
@@ -1,22 +1,28 @@
+const version = 4
+
+const clone = require('clone')
+
module.exports = {
- version: 4,
+ version,
- migrate: function (data) {
+ migrate: function (versionedData) {
+ let safeVersionedData = clone(versionedData)
+ safeVersionedData.meta.version = version
try {
- if (data.config.provider.type !== 'rpc') return data
- switch (data.config.provider.rpcTarget) {
+ if (safeVersionedData.data.config.provider.type !== 'rpc') return Promise.resolve(safeVersionedData)
+ switch (safeVersionedData.data.config.provider.rpcTarget) {
case 'https://testrpc.metamask.io/':
- data.config.provider = {
+ safeVersionedData.data.config.provider = {
type: 'testnet',
}
break
case 'https://rpc.metamask.io/':
- data.config.provider = {
+ safeVersionedData.data.config.provider = {
type: 'mainnet',
}
break
}
} catch (_) {}
- return data
+ return Promise.resolve(safeVersionedData)
},
}
diff --git a/app/scripts/migrations/005.js b/app/scripts/migrations/005.js
new file mode 100644
index 000000000..e4b84f460
--- /dev/null
+++ b/app/scripts/migrations/005.js
@@ -0,0 +1,44 @@
+const version = 5
+
+/*
+
+This migration moves state from the flat state trie into KeyringController substate
+
+*/
+
+const extend = require('xtend')
+const clone = require('clone')
+
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = selectSubstateForKeyringController(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn('MetaMask Migration #5' + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function selectSubstateForKeyringController (state) {
+ const config = state.config
+ const newState = extend(state, {
+ KeyringController: {
+ vault: state.vault,
+ selectedAccount: config.selectedAccount,
+ walletNicknames: state.walletNicknames,
+ },
+ })
+ delete newState.vault
+ delete newState.walletNicknames
+ delete newState.config.selectedAccount
+
+ return newState
+}
diff --git a/app/scripts/migrations/006.js b/app/scripts/migrations/006.js
new file mode 100644
index 000000000..94d1b6ecd
--- /dev/null
+++ b/app/scripts/migrations/006.js
@@ -0,0 +1,43 @@
+const version = 6
+
+/*
+
+This migration moves KeyringController.selectedAddress to PreferencesController.selectedAddress
+
+*/
+
+const extend = require('xtend')
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = migrateState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function migrateState (state) {
+ const keyringSubstate = state.KeyringController
+
+ // add new state
+ const newState = extend(state, {
+ PreferencesController: {
+ selectedAddress: keyringSubstate.selectedAccount,
+ },
+ })
+
+ // rm old state
+ delete newState.KeyringController.selectedAccount
+
+ return newState
+}
diff --git a/app/scripts/migrations/007.js b/app/scripts/migrations/007.js
new file mode 100644
index 000000000..236e35224
--- /dev/null
+++ b/app/scripts/migrations/007.js
@@ -0,0 +1,40 @@
+const version = 7
+
+/*
+
+This migration breaks out the TransactionManager substate
+
+*/
+
+const extend = require('xtend')
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = extend(state, {
+ TransactionManager: {
+ transactions: state.transactions || [],
+ gasMultiplier: state.gasMultiplier || 1,
+ },
+ })
+ delete newState.transactions
+ delete newState.gasMultiplier
+
+ return newState
+}
diff --git a/app/scripts/migrations/008.js b/app/scripts/migrations/008.js
new file mode 100644
index 000000000..cd5e95d22
--- /dev/null
+++ b/app/scripts/migrations/008.js
@@ -0,0 +1,38 @@
+const version = 8
+
+/*
+
+This migration breaks out the NoticeController substate
+
+*/
+
+const extend = require('xtend')
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = extend(state, {
+ NoticeController: {
+ noticesList: state.noticesList || [],
+ },
+ })
+ delete newState.noticesList
+
+ return newState
+}
diff --git a/app/scripts/migrations/009.js b/app/scripts/migrations/009.js
new file mode 100644
index 000000000..4612fefdc
--- /dev/null
+++ b/app/scripts/migrations/009.js
@@ -0,0 +1,43 @@
+const version = 9
+
+/*
+
+This migration breaks out the CurrencyController substate
+
+*/
+
+const merge = require('deep-extend')
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = merge({}, state, {
+ CurrencyController: {
+ currentCurrency: state.currentFiat || state.fiatCurrency || 'USD',
+ conversionRate: state.conversionRate,
+ conversionDate: state.conversionDate,
+ },
+ })
+ delete newState.currentFiat
+ delete newState.fiatCurrency
+ delete newState.conversionRate
+ delete newState.conversionDate
+
+ return newState
+}
diff --git a/app/scripts/migrations/010.js b/app/scripts/migrations/010.js
new file mode 100644
index 000000000..48a841bc1
--- /dev/null
+++ b/app/scripts/migrations/010.js
@@ -0,0 +1,38 @@
+const version = 10
+
+/*
+
+This migration breaks out the CurrencyController substate
+
+*/
+
+const merge = require('deep-extend')
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = merge({}, state, {
+ ShapeShiftController: {
+ shapeShiftTxList: state.shapeShiftTxList || [],
+ },
+ })
+ delete newState.shapeShiftTxList
+
+ return newState
+}
diff --git a/app/scripts/migrations/011.js b/app/scripts/migrations/011.js
new file mode 100644
index 000000000..bf283ef98
--- /dev/null
+++ b/app/scripts/migrations/011.js
@@ -0,0 +1,33 @@
+const version = 11
+
+/*
+
+This migration breaks out the CurrencyController substate
+
+*/
+
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ let versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = state
+ delete newState.TOSHash
+ delete newState.isDisclaimerConfirmed
+ return newState
+}
diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js
new file mode 100644
index 000000000..04c966d4d
--- /dev/null
+++ b/app/scripts/migrations/_multi-keyring.js
@@ -0,0 +1,51 @@
+const version = 5
+
+/*
+
+This is an incomplete migration bc it requires post-decrypted data
+which we dont have access to at the time of this writing.
+
+*/
+
+const ObservableStore = require('obs-store')
+const ConfigManager = require('../../app/scripts/lib/config-manager')
+const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator')
+const KeyringController = require('../../app/scripts/lib/keyring-controller')
+
+const password = 'obviously not correct'
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ versionedData.meta.version = version
+
+ let store = new ObservableStore(versionedData.data)
+ let configManager = new ConfigManager({ store })
+ let idStoreMigrator = new IdentityStoreMigrator({ configManager })
+ let keyringController = new KeyringController({
+ configManager: configManager,
+ })
+
+ // attempt to migrate to multiVault
+ return idStoreMigrator.migratedVaultForPassword(password)
+ .then((result) => {
+ // skip if nothing to migrate
+ if (!result) return Promise.resolve(versionedData)
+ delete versionedData.data.wallet
+ // create new keyrings
+ const privKeys = result.lostAccounts.map(acct => acct.privateKey)
+ return Promise.all([
+ keyringController.restoreKeyring(result.serialized),
+ keyringController.restoreKeyring({ type: 'Simple Key Pair', data: privKeys }),
+ ]).then(() => {
+ return keyringController.persistAllKeyrings(password)
+ }).then(() => {
+ // copy result on to state object
+ versionedData.data = store.get()
+ return Promise.resolve(versionedData)
+ })
+ })
+
+ },
+}
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
new file mode 100644
index 000000000..a3dd48c17
--- /dev/null
+++ b/app/scripts/migrations/index.js
@@ -0,0 +1,25 @@
+/* The migrator has two methods the user should be concerned with:
+ *
+ * getData(), which returns the app-consumable data object
+ * saveData(), which persists the app-consumable data object.
+ */
+
+// Migrations must start at version 1 or later.
+// They are objects with a `version` number
+// and a `migrate` function.
+//
+// The `migrate` function receives the previous
+// config data format, and returns the new one.
+
+module.exports = [
+ require('./002'),
+ require('./003'),
+ require('./004'),
+ require('./005'),
+ require('./006'),
+ require('./007'),
+ require('./008'),
+ require('./009'),
+ require('./010'),
+ require('./011'),
+]
diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js
new file mode 100644
index 000000000..0d72760fe
--- /dev/null
+++ b/app/scripts/notice-controller.js
@@ -0,0 +1,94 @@
+const EventEmitter = require('events').EventEmitter
+const extend = require('xtend')
+const ObservableStore = require('obs-store')
+const hardCodedNotices = require('../../notices/notices.json')
+
+module.exports = class NoticeController extends EventEmitter {
+
+ constructor (opts) {
+ super()
+ this.noticePoller = null
+ const initState = extend({
+ noticesList: [],
+ }, opts.initState)
+ this.store = new ObservableStore(initState)
+ this.memStore = new ObservableStore({})
+ this.store.subscribe(() => this._updateMemstore())
+ }
+
+ getNoticesList () {
+ return this.store.getState().noticesList
+ }
+
+ getUnreadNotices () {
+ const notices = this.getNoticesList()
+ return notices.filter((notice) => notice.read === false)
+ }
+
+ getLatestUnreadNotice () {
+ const unreadNotices = this.getUnreadNotices()
+ return unreadNotices[unreadNotices.length - 1]
+ }
+
+ setNoticesList (noticesList) {
+ this.store.updateState({ noticesList })
+ return Promise.resolve(true)
+ }
+
+ markNoticeRead (noticeToMark, cb) {
+ cb = cb || function (err) { if (err) throw err }
+ try {
+ var notices = this.getNoticesList()
+ var index = notices.findIndex((currentNotice) => currentNotice.id === noticeToMark.id)
+ notices[index].read = true
+ this.setNoticesList(notices)
+ const latestNotice = this.getLatestUnreadNotice()
+ cb(null, latestNotice)
+ } catch (err) {
+ cb(err)
+ }
+ }
+
+ updateNoticesList () {
+ return this._retrieveNoticeData().then((newNotices) => {
+ var oldNotices = this.getNoticesList()
+ var combinedNotices = this._mergeNotices(oldNotices, newNotices)
+ return Promise.resolve(this.setNoticesList(combinedNotices))
+ })
+ }
+
+ startPolling () {
+ if (this.noticePoller) {
+ clearInterval(this.noticePoller)
+ }
+ this.noticePoller = setInterval(() => {
+ this.noticeController.updateNoticesList()
+ }, 300000)
+ }
+
+ _mergeNotices (oldNotices, newNotices) {
+ var noticeMap = this._mapNoticeIds(oldNotices)
+ newNotices.forEach((notice) => {
+ if (noticeMap.indexOf(notice.id) === -1) {
+ oldNotices.push(notice)
+ }
+ })
+ return oldNotices
+ }
+
+ _mapNoticeIds (notices) {
+ return notices.map((notice) => notice.id)
+ }
+
+ _retrieveNoticeData () {
+ // Placeholder for the API.
+ return Promise.resolve(hardCodedNotices)
+ }
+
+ _updateMemstore () {
+ const lastUnreadNotice = this.getLatestUnreadNotice()
+ const noActiveNotices = !lastUnreadNotice
+ this.memStore.updateState({ lastUnreadNotice, noActiveNotices })
+ }
+
+}
diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js
new file mode 100644
index 000000000..0c97a5d19
--- /dev/null
+++ b/app/scripts/popup-core.js
@@ -0,0 +1,63 @@
+const EventEmitter = require('events').EventEmitter
+const Dnode = require('dnode')
+const Web3 = require('web3')
+const MetaMaskUi = require('../../ui')
+const StreamProvider = require('web3-stream-provider')
+const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
+
+
+module.exports = initializePopup
+
+
+function initializePopup (connectionStream) {
+ // setup app
+ connectToAccountManager(connectionStream, setupApp)
+}
+
+function connectToAccountManager (connectionStream, cb) {
+ // setup communication with background
+ // setup multiplexing
+ var mx = setupMultiplex(connectionStream)
+ // connect features
+ setupControllerConnection(mx.createStream('controller'), cb)
+ setupWeb3Connection(mx.createStream('provider'))
+}
+
+function setupWeb3Connection (connectionStream) {
+ var providerStream = new StreamProvider()
+ providerStream.pipe(connectionStream).pipe(providerStream)
+ connectionStream.on('error', console.error.bind(console))
+ providerStream.on('error', console.error.bind(console))
+ global.web3 = new Web3(providerStream)
+}
+
+function setupControllerConnection (connectionStream, cb) {
+ // this is a really sneaky way of adding EventEmitter api
+ // to a bi-directional dnode instance
+ var eventEmitter = new EventEmitter()
+ var accountManagerDnode = Dnode({
+ sendUpdate: function (state) {
+ eventEmitter.emit('update', state)
+ },
+ })
+ connectionStream.pipe(accountManagerDnode).pipe(connectionStream)
+ accountManagerDnode.once('remote', function (accountManager) {
+ // setup push events
+ accountManager.on = eventEmitter.on.bind(eventEmitter)
+ cb(null, accountManager)
+ })
+}
+
+function setupApp (err, accountManager) {
+ if (err) {
+ alert(err.stack)
+ throw err
+ }
+
+ var container = document.getElementById('app-content')
+
+ MetaMaskUi({
+ container: container,
+ accountManager: accountManager,
+ })
+}
diff --git a/app/scripts/popup.js b/app/scripts/popup.js
index 20be15df7..62db68c10 100644
--- a/app/scripts/popup.js
+++ b/app/scripts/popup.js
@@ -1,95 +1,25 @@
-const url = require('url')
-const EventEmitter = require('events').EventEmitter
-const async = require('async')
-const Dnode = require('dnode')
-const Web3 = require('web3')
-const MetaMaskUi = require('../../ui')
-const MetaMaskUiCss = require('../../ui/css')
const injectCss = require('inject-css')
+const MetaMaskUiCss = require('../../ui/css')
+const startPopup = require('./popup-core')
const PortStream = require('./lib/port-stream.js')
-const StreamProvider = require('web3-stream-provider')
-const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
+const isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('./lib/extension')
+const notification = require('./lib/notifications')
-// setup app
var css = MetaMaskUiCss()
injectCss(css)
-async.parallel({
- currentDomain: getCurrentDomain,
- accountManager: connectToAccountManager,
-}, setupApp)
+var name = isPopupOrNotification()
+closePopupIfOpen(name)
+window.METAMASK_UI_TYPE = name
-function connectToAccountManager (cb) {
- // setup communication with background
- var pluginPort = extension.runtime.connect({name: 'popup'})
- var portStream = new PortStream(pluginPort)
- // setup multiplexing
- var mx = setupMultiplex(portStream)
- // connect features
- setupControllerConnection(mx.createStream('controller'), cb)
- setupWeb3Connection(mx.createStream('provider'))
-}
+var pluginPort = extension.runtime.connect({ name })
+var portStream = new PortStream(pluginPort)
-function setupWeb3Connection (stream) {
- var remoteProvider = new StreamProvider()
- remoteProvider.pipe(stream).pipe(remoteProvider)
- stream.on('error', console.error.bind(console))
- remoteProvider.on('error', console.error.bind(console))
- global.web3 = new Web3(remoteProvider)
-}
+startPopup(portStream)
-function setupControllerConnection (stream, cb) {
- var eventEmitter = new EventEmitter()
- var background = Dnode({
- sendUpdate: function (state) {
- eventEmitter.emit('update', state)
- },
- })
- stream.pipe(background).pipe(stream)
- background.once('remote', function (accountManager) {
- // setup push events
- accountManager.on = eventEmitter.on.bind(eventEmitter)
- cb(null, accountManager)
- })
-}
-
-function getCurrentDomain (cb) {
- const unknown = '<unknown>'
- if (!extension.tabs) return cb(null, unknown)
- extension.tabs.query({active: true, currentWindow: true}, function (results) {
- var activeTab = results[0]
- var currentUrl = activeTab && activeTab.url
- var currentDomain = url.parse(currentUrl).host
- if (!currentUrl) {
- return cb(null, unknown)
- }
- cb(null, currentDomain)
- })
-}
-
-function clearNotifications(){
- extension.notifications.getAll(function (object) {
- for (let notification in object){
- extension.notifications.clear(notification)
- }
- })
-}
-
-function setupApp (err, opts) {
- if (err) {
- alert(err.stack)
- throw err
+function closePopupIfOpen (name) {
+ if (name !== 'notification') {
+ notification.closePopup()
}
-
- clearNotifications()
-
- var container = document.getElementById('app-content')
-
- MetaMaskUi({
- container: container,
- accountManager: opts.accountManager,
- currentDomain: opts.currentDomain,
- networkVersion: opts.networkVersion,
- })
}
diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js
new file mode 100644
index 000000000..6299091f2
--- /dev/null
+++ b/app/scripts/transaction-manager.js
@@ -0,0 +1,390 @@
+const EventEmitter = require('events')
+const async = require('async')
+const extend = require('xtend')
+const Semaphore = require('semaphore')
+const ObservableStore = require('obs-store')
+const ethUtil = require('ethereumjs-util')
+const BN = require('ethereumjs-util').BN
+const TxProviderUtil = require('./lib/tx-utils')
+const createId = require('./lib/random-id')
+
+module.exports = class TransactionManager extends EventEmitter {
+ constructor (opts) {
+ super()
+ this.store = new ObservableStore(extend({
+ transactions: [],
+ gasMultiplier: 1,
+ }, opts.initState))
+ this.memStore = new ObservableStore({})
+ this.networkStore = opts.networkStore || new ObservableStore({})
+ this.preferencesStore = opts.preferencesStore || new ObservableStore({})
+ this.txHistoryLimit = opts.txHistoryLimit
+ this.provider = opts.provider
+ this.blockTracker = opts.blockTracker
+ this.txProviderUtils = new TxProviderUtil(this.provider)
+ this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
+ this.signEthTx = opts.signTransaction
+ this.nonceLock = Semaphore(1)
+
+ // memstore is computed from a few different stores
+ this._updateMemstore()
+ this.store.subscribe(() => this._updateMemstore() )
+ this.networkStore.subscribe(() => this._updateMemstore() )
+ this.preferencesStore.subscribe(() => this._updateMemstore() )
+ }
+
+ getState () {
+ return this.memStore.getState()
+ }
+
+ getNetwork () {
+ return this.networkStore.getState().network
+ }
+
+ getSelectedAddress () {
+ return this.preferencesStore.getState().selectedAddress
+ }
+
+ // Returns the tx list
+ getTxList () {
+ let network = this.getNetwork()
+ let fullTxList = this.store.getState().transactions
+ return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network)
+ }
+
+ getGasMultiplier () {
+ return this.store.getState().gasMultiplier
+ }
+
+ setGasMultiplier (gasMultiplier) {
+ return this.store.updateState({ gasMultiplier })
+ }
+
+ // Adds a tx to the txlist
+ addTx (txMeta) {
+ var txList = this.getTxList()
+ var txHistoryLimit = this.txHistoryLimit
+
+ // checks if the length of th tx history is
+ // longer then desired persistence limit
+ // and then if it is removes only confirmed
+ // or rejected tx's.
+ // not tx's that are pending or unapproved
+ if (txList.length > txHistoryLimit - 1) {
+ var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
+ txList.splice(index, 1)
+ }
+ txList.push(txMeta)
+
+ this._saveTxList(txList)
+ this.once(`${txMeta.id}:signed`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:rejected`)
+ })
+ this.once(`${txMeta.id}:rejected`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:signed`)
+ })
+
+ this.emit('updateBadge')
+ this.emit(`${txMeta.id}:unapproved`, txMeta)
+ }
+
+ // gets tx by Id and returns it
+ getTx (txId, cb) {
+ var txList = this.getTxList()
+ var txMeta = txList.find(txData => txData.id === txId)
+ return cb ? cb(txMeta) : txMeta
+ }
+
+ //
+ updateTx (txMeta) {
+ var txId = txMeta.id
+ var txList = this.getTxList()
+ var index = txList.findIndex(txData => txData.id === txId)
+ txList[index] = txMeta
+ this._saveTxList(txList)
+ this.emit('update')
+ }
+
+ get unapprovedTxCount () {
+ return Object.keys(this.getUnapprovedTxList()).length
+ }
+
+ get pendingTxCount () {
+ return this.getTxsByMetaData('status', 'signed').length
+ }
+
+ addUnapprovedTransaction (txParams, done) {
+ let txMeta
+ async.waterfall([
+ // validate
+ (cb) => this.txProviderUtils.validateTxParams(txParams, cb),
+ // prepare txMeta
+ (cb) => {
+ // create txMeta obj with parameters and meta data
+ let time = (new Date()).getTime()
+ let txId = createId()
+ txParams.metamaskId = txId
+ txParams.metamaskNetworkId = this.getNetwork()
+ txMeta = {
+ id: txId,
+ time: time,
+ status: 'unapproved',
+ gasMultiplier: this.getGasMultiplier(),
+ metamaskNetworkId: this.getNetwork(),
+ txParams: txParams,
+ }
+ // calculate metadata for tx
+ this.txProviderUtils.analyzeGasUsage(txMeta, cb)
+ },
+ // save txMeta
+ (cb) => {
+ this.addTx(txMeta)
+ this.setMaxTxCostAndFee(txMeta)
+ cb(null, txMeta)
+ },
+ ], done)
+ }
+
+ setMaxTxCostAndFee (txMeta) {
+ var txParams = txMeta.txParams
+ var gasMultiplier = txMeta.gasMultiplier
+ var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16)
+ var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16)
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100), 10).div(new BN(100, 10))
+ var txFee = gasCost.mul(gasPrice)
+ var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
+ var maxCost = txValue.add(txFee)
+ txMeta.txFee = txFee
+ txMeta.txValue = txValue
+ txMeta.maxCost = maxCost
+ this.updateTx(txMeta)
+ }
+
+ getUnapprovedTxList () {
+ var txList = this.getTxList()
+ return txList.filter((txMeta) => txMeta.status === 'unapproved')
+ .reduce((result, tx) => {
+ result[tx.id] = tx
+ return result
+ }, {})
+ }
+
+ approveTransaction (txId, cb = warn) {
+ const self = this
+ // approve
+ self.setTxStatusApproved(txId)
+ // only allow one tx at a time for atomic nonce usage
+ self.nonceLock.take(() => {
+ // begin signature process
+ async.waterfall([
+ (cb) => self.fillInTxParams(txId, cb),
+ (cb) => self.signTransaction(txId, cb),
+ (rawTx, cb) => self.publishTransaction(txId, rawTx, cb),
+ ], (err) => {
+ self.nonceLock.leave()
+ if (err) {
+ this.setTxStatusFailed(txId)
+ return cb(err)
+ }
+ cb()
+ })
+ })
+ }
+
+ cancelTransaction (txId, cb = warn) {
+ this.setTxStatusRejected(txId)
+ cb()
+ }
+
+ fillInTxParams (txId, cb) {
+ let txMeta = this.getTx(txId)
+ this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => {
+ if (err) return cb(err)
+ this.updateTx(txMeta)
+ cb()
+ })
+ }
+
+ signTransaction (txId, cb) {
+ let txMeta = this.getTx(txId)
+ let txParams = txMeta.txParams
+ let fromAddress = txParams.from
+ let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams, txMeta.gasMultiplier)
+ this.signEthTx(ethTx, fromAddress).then(() => {
+ this.setTxStatusSigned(txMeta.id)
+ cb(null, ethUtil.bufferToHex(ethTx.serialize()))
+ }).catch((err) => {
+ cb(err)
+ })
+ }
+
+ publishTransaction (txId, rawTx, cb) {
+ this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => {
+ if (err) return cb(err)
+ this.setTxHash(txId, txHash)
+ this.setTxStatusSubmitted(txId)
+ cb()
+ })
+ }
+
+ // receives a txHash records the tx as signed
+ setTxHash (txId, txHash) {
+ // Add the tx hash to the persisted meta-tx object
+ let txMeta = this.getTx(txId)
+ txMeta.hash = txHash
+ this.updateTx(txMeta)
+ }
+
+ /*
+ Takes an object of fields to search for eg:
+ var thingsToLookFor = {
+ to: '0x0..',
+ from: '0x0..',
+ status: 'signed',
+ }
+ and returns a list of tx with all
+ options matching
+
+ this is for things like filtering a the tx list
+ for only tx's from 1 account
+ or for filltering for all txs from one account
+ and that have been 'confirmed'
+ */
+ getFilteredTxList (opts) {
+ var filteredTxList
+ Object.keys(opts).forEach((key) => {
+ filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
+ })
+ return filteredTxList
+ }
+
+ getTxsByMetaData (key, value, txList = this.getTxList()) {
+ return txList.filter((txMeta) => {
+ if (txMeta.txParams[key]) {
+ return txMeta.txParams[key] === value
+ } else {
+ return txMeta[key] === value
+ }
+ })
+ }
+
+ // STATUS METHODS
+ // get::set status
+
+ // should return the status of the tx.
+ getTxStatus (txId) {
+ const txMeta = this.getTx(txId)
+ return txMeta.status
+ }
+
+ // should update the status of the tx to 'rejected'.
+ setTxStatusRejected (txId) {
+ this._setTxStatus(txId, 'rejected')
+ }
+
+ // should update the status of the tx to 'approved'.
+ setTxStatusApproved (txId) {
+ this._setTxStatus(txId, 'approved')
+ }
+
+ // should update the status of the tx to 'signed'.
+ setTxStatusSigned (txId) {
+ this._setTxStatus(txId, 'signed')
+ }
+
+ // should update the status of the tx to 'submitted'.
+ setTxStatusSubmitted (txId) {
+ this._setTxStatus(txId, 'submitted')
+ }
+
+ // should update the status of the tx to 'confirmed'.
+ setTxStatusConfirmed (txId) {
+ this._setTxStatus(txId, 'confirmed')
+ }
+
+ setTxStatusFailed (txId) {
+ this._setTxStatus(txId, 'failed')
+ }
+
+ // merges txParams obj onto txData.txParams
+ // use extend to ensure that all fields are filled
+ updateTxParams (txId, txParams) {
+ var txMeta = this.getTx(txId)
+ txMeta.txParams = extend(txMeta.txParams, txParams)
+ this.updateTx(txMeta)
+ }
+
+ // checks if a signed tx is in a block and
+ // if included sets the tx status as 'confirmed'
+ checkForTxInBlock () {
+ var signedTxList = this.getFilteredTxList({status: 'submitted'})
+ if (!signedTxList.length) return
+ signedTxList.forEach((txMeta) => {
+ var txHash = txMeta.hash
+ var txId = txMeta.id
+ if (!txHash) {
+ txMeta.err = {
+ errCode: 'No hash was provided',
+ message: 'We had an error while submitting this transaction, please try again.',
+ }
+ this.updateTx(txMeta)
+ return this.setTxStatusFailed(txId)
+ }
+ this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => {
+ if (err || !txParams) {
+ if (!txParams) return
+ txMeta.err = {
+ isWarning: true,
+ errorCode: err,
+ message: 'There was a problem loading this transaction.',
+ }
+ this.updateTx(txMeta)
+ return console.error(err)
+ }
+ if (txParams.blockNumber) {
+ this.setTxStatusConfirmed(txId)
+ }
+ })
+ })
+ }
+
+ // PRIVATE METHODS
+
+ // Should find the tx in the tx list and
+ // update it.
+ // should set the status in txData
+ // - `'unapproved'` the user has not responded
+ // - `'rejected'` the user has responded no!
+ // - `'approved'` the user has approved the tx
+ // - `'signed'` the tx is signed
+ // - `'submitted'` the tx is sent to a server
+ // - `'confirmed'` the tx has been included in a block.
+ _setTxStatus (txId, status) {
+ var txMeta = this.getTx(txId)
+ txMeta.status = status
+ this.emit(`${txMeta.id}:${status}`, txId)
+ if (status === 'submitted' || status === 'rejected') {
+ this.emit(`${txMeta.id}:finished`, status)
+ }
+ this.updateTx(txMeta)
+ this.emit('updateBadge')
+ }
+
+ // Saves the new/updated txList.
+ // Function is intended only for internal use
+ _saveTxList (transactions) {
+ this.store.updateState({ transactions })
+ }
+
+ _updateMemstore () {
+ const unapprovedTxs = this.getUnapprovedTxList()
+ const selectedAddressTxList = this.getFilteredTxList({
+ from: this.getSelectedAddress(),
+ metamaskNetworkId: this.getNetwork(),
+ })
+ this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
+ }
+}
+
+
+const warn = () => console.warn('warn was used no cb provided')