aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/account-import-strategies/index.js45
-rw-r--r--app/scripts/background.js253
-rw-r--r--app/scripts/first-time-state.js11
-rw-r--r--app/scripts/inpage.js4
-rw-r--r--app/scripts/keyring-controller.js311
-rw-r--r--app/scripts/keyrings/hd.js11
-rw-r--r--app/scripts/keyrings/simple.js11
-rw-r--r--app/scripts/lib/config-manager.js156
-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.js220
-rw-r--r--app/scripts/lib/idStore.js4
-rw-r--r--app/scripts/lib/inpage-provider.js79
-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/remote-store.js97
-rw-r--r--app/scripts/lib/stream-utils.js9
-rw-r--r--app/scripts/metamask-controller.js803
-rw-r--r--app/scripts/migrations/002.js15
-rw-r--r--app/scripts/migrations/003.js16
-rw-r--r--app/scripts/migrations/004.js17
-rw-r--r--app/scripts/migrations/005.js41
-rw-r--r--app/scripts/migrations/006.js41
-rw-r--r--app/scripts/migrations/007.js38
-rw-r--r--app/scripts/migrations/008.js36
-rw-r--r--app/scripts/migrations/009.js40
-rw-r--r--app/scripts/migrations/010.js36
-rw-r--r--app/scripts/migrations/_multi-keyring.js51
-rw-r--r--app/scripts/migrations/index.js24
-rw-r--r--app/scripts/notice-controller.js52
-rw-r--r--app/scripts/transaction-manager.js65
34 files changed, 1698 insertions, 1204 deletions
diff --git a/app/manifest.json b/app/manifest.json
index 8662c8030..8d6fb2ac4 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.1.2",
+ "version": "3.2.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
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 f3837a028..2e5a992b9 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -1,162 +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 notification = require('./lib/notifications.js')
-const messageManager = require('./lib/message-manager')
-const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
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'
-var popupIsOpen = false
-
-const controller = new MetamaskController({
- // User confirmation callbacks:
- showUnconfirmedMessage: triggerUi,
- unlockAccountMessage: triggerUi,
- showUnapprovedTx: triggerUi,
- // Persistence Methods:
- setData,
- loadData,
-})
-function triggerUi () {
- if (!popupIsOpen) notification.show()
-}
-// 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'})
- }
-})
+let popupIsOpen = false
+
+// state persistence
+const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
+
+// initialization flow
+asyncQ.waterfall([
+ () => loadStateFromPersistence(),
+ (initState) => setupController(initState),
+])
+.then(() => console.log('MetaMask initialization complete.'))
+.catch((err) => { console.error(err) })
//
-// connect to other contexts
+// State and Persistence
//
-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 = remotePort.name === 'popup'
- setupTrustedCommunication(portStream, 'MetaMask', remotePort.name)
- } else {
- // communication with page
- var originDomain = urlUtil.parse(remotePort.sender.url).hostname
- setupUntrustedCommunication(portStream, originDomain)
- }
-}
-
-function setupUntrustedCommunication (connectionStream, originDomain) {
- // setup multiplexing
- var mx = setupMultiplex(connectionStream)
- // connect features
- controller.setupProviderConnection(mx.createStream('provider'), originDomain)
- controller.setupPublicConfig(mx.createStream('publicConfig'))
+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)
+ },
+ // resolve to just data
+ (versionedData) => Promise.resolve(versionedData.data),
+ ])
}
-function setupTrustedCommunication (connectionStream, originDomain) {
- // setup multiplexing
- var mx = setupMultiplex(connectionStream)
- // connect features
- setupControllerConnection(mx.createStream('controller'))
- controller.setupProviderConnection(mx.createStream('provider'), originDomain)
-}
+function setupController (initState) {
-//
-// remote features
-//
+ //
+ // MetaMask Controller
+ //
-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
- var sendUpdate = remote.sendUpdate.bind(remote)
- controller.on('update', sendUpdate)
- // teardown on disconnect
- eos(stream, () => {
- controller.removeListener('update', sendUpdate)
- popupIsOpen = false
- })
+ const controller = new MetamaskController({
+ // User confirmation callbacks:
+ showUnconfirmedMessage: triggerUi,
+ unlockAccountMessage: triggerUi,
+ showUnapprovedTx: triggerUi,
+ // initial state
+ initState,
})
-}
-
-//
-// plugin badge text
-//
+ global.metamaskController = controller
+
+ // setup state persistence
+ pipe(
+ controller.store,
+ storeTransform(versionifyData),
+ diskStore
+ )
+
+ function versionifyData(state) {
+ let versionedData = diskStore.getState()
+ versionedData.data = state
+ return versionedData
+ }
-controller.txManager.on('updateBadge', updateBadge)
-updateBadge()
-
-function updateBadge () {
- var label = ''
- var unapprovedTxCount = controller.txManager.unapprovedTxCount
- var unconfMsgs = messageManager.unconfirmedMsgs()
- var unconfMsgLen = Object.keys(unconfMsgs).length
- var count = unapprovedTxCount + unconfMsgLen
- if (count) {
- label = String(count)
+ //
+ // 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)
+ }
}
- extension.browserAction.setBadgeText({ text: label })
- extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
-}
-// data :: setters/getters
+ //
+ // 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' })
+ }
-function loadData () {
- var oldData = getOldStyleData()
- var newData
- try {
- newData = JSON.parse(window.localStorage[STORAGE_KEY])
- } catch (e) {}
+ return Promise.resolve()
- 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: {},
- }
+//
+// Etc...
+//
- 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
+// popup trigger
+function triggerUi () {
+ if (!popupIsOpen) notification.show()
}
-function setData (data) {
- window.localStorage[STORAGE_KEY] = JSON.stringify(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'})
+ }
+})
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 42332d92e..419f78cd6 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -50,9 +50,9 @@ reloadStream.once('data', triggerReload)
// })
// endOfStream(pingStream, triggerReload)
-// set web3 defaultAcount
+// set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) {
- web3.eth.defaultAccount = state.selectedAccount
+ web3.eth.defaultAccount = state.selectedAddress
})
//
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index 76422bf6b..348f81fc9 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -1,13 +1,11 @@
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 normalize = require('./lib/sig-util').normalize
-const messageManager = require('./lib/message-manager')
-const BN = ethUtil.BN
-
+const normalizeAddress = require('./lib/sig-util').normalize
// Keyrings:
const SimpleKeyring = require('./keyrings/simple')
const HdKeyring = require('./keyrings/hd')
@@ -16,9 +14,7 @@ const keyringTypes = [
HdKeyring,
]
-const createId = require('./lib/random-id')
-
-module.exports = class KeyringController extends EventEmitter {
+class KeyringController extends EventEmitter {
// PUBLIC METHODS
//
@@ -29,29 +25,21 @@ module.exports = class KeyringController extends EventEmitter {
constructor (opts) {
super()
- this.configManager = opts.configManager
+ 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.keyringTypes = keyringTypes
this.keyrings = []
- this.identities = {} // Essentially a name hash
-
- this._unconfMsgCbs = {}
-
this.getNetwork = opts.getNetwork
}
- // Set Store
- //
- // Allows setting the ethStore after the constructor.
- // This is currently required because of the initialization order
- // of the ethStore and this class.
- //
- // Eventually would be nice to be able to add this in the constructor.
- setStore (ethStore) {
- this.ethStore = ethStore
- }
-
// Full Update
// returns Promise( @object state )
//
@@ -65,48 +53,7 @@ module.exports = class KeyringController extends EventEmitter {
// Not all methods end with this, that might be a nice refactor.
fullUpdate () {
this.emit('update')
- return Promise.resolve(this.getState())
- }
-
- // Get State
- // returns @object state
- //
- // This method returns a hash representing the current state
- // that the keyringController manages.
- //
- // It is extended in the MetamaskController along with the EthStore
- // state, and its own state, to create the metamask state branch
- // that is passed to the UI.
- //
- // This is currently a rare example of a synchronously resolving method
- // in this class, but will need to be Promisified when we move our
- // persistence to an async model.
- getState () {
- const configManager = this.configManager
- const address = configManager.getSelectedAccount()
- const wallet = configManager.getWallet() // old style vault
- const vault = configManager.getVault() // new style vault
- const keyrings = this.keyrings
-
- return Promise.all(keyrings.map(this.displayForKeyring))
- .then((displayKeyrings) => {
- return {
- seedWords: this.configManager.getSeedWords(),
- isInitialized: (!!wallet || !!vault),
- isUnlocked: Boolean(this.password),
- isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(),
- unconfMsgs: messageManager.unconfirmedMsgs(),
- messages: messageManager.getMsgList(),
- selectedAccount: address,
- shapeShiftTxList: this.configManager.getShapeShiftTxList(),
- currentFiat: this.configManager.getCurrentFiat(),
- conversionRate: this.configManager.getConversionRate(),
- conversionDate: this.configManager.getConversionDate(),
- keyringTypes: this.keyringTypes.map(krt => krt.type),
- identities: this.identities,
- keyrings: displayKeyrings,
- }
- })
+ return Promise.resolve(this.memStore.getState())
}
// Create New Vault And Keychain
@@ -150,57 +97,32 @@ module.exports = class KeyringController extends EventEmitter {
mnemonic: seed,
numberOfAccounts: 1,
})
- }).then(() => {
- const firstKeyring = this.keyrings[0]
+ })
+ .then((firstKeyring) => {
return firstKeyring.getAccounts()
})
.then((accounts) => {
const firstAccount = accounts[0]
- const hexAccount = normalize(firstAccount)
- this.configManager.setSelectedAccount(hexAccount)
+ 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))
}
- // PlaceSeedWords
- // returns Promise( @object state )
- //
- // 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 () {
- const hdKeyrings = this.keyrings.filter((keyring) => keyring.type === 'HD Key Tree')
- const firstKeyring = hdKeyrings[0]
- if (!firstKeyring) throw new Error('KeyringController - No HD Key Tree found')
- return firstKeyring.serialize()
- .then((serialized) => {
- const seedWords = serialized.mnemonic
- this.configManager.setSeedWords(seedWords)
- return this.fullUpdate()
- })
- }
-
- // ClearSeedWordCache
- //
- // returns Promise( @string currentSelectedAccount )
- //
- // Removes the current vault's seed words from the UI's state tree,
- // ensuring they are only ever available in the background process.
- clearSeedWordCache () {
- this.configManager.setSeedWords(null)
- return Promise.resolve(this.configManager.getSelectedAccount())
- }
-
// 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()
}
@@ -244,8 +166,8 @@ module.exports = class KeyringController extends EventEmitter {
this.keyrings.push(keyring)
return this.setupAccounts(accounts)
})
- .then(() => { return this.password })
- .then(this.persistAllKeyrings.bind(this))
+ .then(() => this.persistAllKeyrings())
+ .then(() => this.fullUpdate())
.then(() => {
return keyring
})
@@ -259,29 +181,13 @@ module.exports = class KeyringController extends EventEmitter {
// Calls the `addAccounts` method on the Keyring
// in the kryings array at index `keyringNum`,
// and then saves those changes.
- addNewAccount () {
- const hdKeyrings = this.keyrings.filter((keyring) => keyring.type === 'HD Key Tree')
- const firstKeyring = hdKeyrings[0]
- if (!firstKeyring) throw new Error('KeyringController - No HD Key Tree found')
- return firstKeyring.addAccounts(1)
+ addNewAccount (selectedKeyring) {
+ return selectedKeyring.addAccounts(1)
.then(this.setupAccounts.bind(this))
.then(this.persistAllKeyrings.bind(this))
.then(this.fullUpdate.bind(this))
}
- // Set Selected Account
- // @string address
- //
- // returns Promise( @string address )
- //
- // Sets the state's `selectedAccount` value
- // to the specified address.
- setSelectedAccount (address) {
- var addr = normalize(address)
- this.configManager.setSelectedAccount(addr)
- return this.fullUpdate()
- }
-
// Save Account Label
// @string account
// @string label
@@ -290,11 +196,21 @@ module.exports = class KeyringController extends EventEmitter {
//
// Persists a nickname equal to `label` for the specified account.
saveAccountLabel (account, label) {
- const address = normalize(account)
- const configManager = this.configManager
- configManager.setNicknameForWallet(address, label)
- this.identities[address].name = label
- return Promise.resolve(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
@@ -310,7 +226,7 @@ module.exports = class KeyringController extends EventEmitter {
try {
return this.getKeyringForAccount(address)
.then((keyring) => {
- return keyring.exportAccount(normalize(address))
+ return keyring.exportAccount(normalizeAddress(address))
})
} catch (e) {
return Promise.reject(e)
@@ -324,92 +240,25 @@ module.exports = class KeyringController extends EventEmitter {
// TX Manager to update the state after signing
signTransaction (ethTx, _fromAddress) {
- const fromAddress = normalize(_fromAddress)
+ const fromAddress = normalizeAddress(_fromAddress)
return this.getKeyringForAccount(fromAddress)
.then((keyring) => {
return keyring.signTransaction(fromAddress, ethTx)
})
}
- // Add Unconfirmed Message
- // @object msgParams
- // @function cb
- //
- // Does not call back, only emits an `update` event.
- //
- // Adds the given `msgParams` and `cb` to a local cache,
- // for displaying to a user for approval before signing or canceling.
- addUnconfirmedMessage (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.emit('update')
- return msgId
- }
-
- // Cancel Message
- // @string msgId
- // @function cb (optional)
- //
- // Calls back to cached `unconfMsgCb`.
- // Calls back to `cb` if provided.
- //
- // Forgets any messages matching `msgId`.
- cancelMessage (msgId, cb) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- messageManager.rejectMsg(msgId)
- delete this._unconfTxCbs[msgId]
-
- if (cb && typeof cb === 'function') {
- cb()
- }
- }
// Sign Message
// @object msgParams
- // @function cb
//
// returns Promise(@buffer rawSig)
- // calls back @function cb with @buffer rawSig
- // calls back cached Dapp's @function unconfMsgCb.
//
// Attempts to sign the provided @object msgParams.
- signMessage (msgParams, cb) {
- try {
- const msgId = msgParams.metamaskId
- delete msgParams.metamaskId
- const approvalCb = this._unconfMsgCbs[msgId] || noop
-
- const address = normalize(msgParams.from)
- return this.getKeyringForAccount(address)
- .then((keyring) => {
- return keyring.signMessage(address, msgParams.data)
- }).then((rawSig) => {
- cb(null, rawSig)
- approvalCb(null, true)
- messageManager.confirmMsg(msgId)
- return rawSig
- })
- } catch (e) {
- cb(e)
- }
+ signMessage (msgParams) {
+ const address = normalizeAddress(msgParams.from)
+ return this.getKeyringForAccount(address)
+ .then((keyring) => {
+ return keyring.signMessage(address, msgParams.data)
+ })
}
// PRIVATE METHODS
@@ -428,18 +277,16 @@ module.exports = class KeyringController extends EventEmitter {
// puts the current seed words into the state tree.
createFirstKeyTree () {
this.clearKeyrings()
- return this.addNewKeyring('HD Key Tree', {numberOfAccounts: 1})
- .then(() => {
- return this.keyrings[0].getAccounts()
+ return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
+ .then((keyring) => {
+ return keyring.getAccounts()
})
.then((accounts) => {
const firstAccount = accounts[0]
- const hexAccount = normalize(firstAccount)
- this.configManager.setSelectedAccount(hexAccount)
+ if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
+ const hexAccount = normalizeAddress(firstAccount)
this.emit('newAccount', hexAccount)
return this.setupAccounts(accounts)
- }).then(() => {
- return this.placeSeedWords()
})
.then(this.persistAllKeyrings.bind(this))
}
@@ -473,7 +320,7 @@ module.exports = class KeyringController extends EventEmitter {
if (!account) {
throw new Error('Problem loading account.')
}
- const address = normalize(account)
+ const address = normalizeAddress(account)
this.ethStore.addAccount(address)
return this.createNickname(address)
}
@@ -485,14 +332,17 @@ module.exports = class KeyringController extends EventEmitter {
//
// Takes an address, and assigns it an incremented nickname, persisting it.
createNickname (address) {
- const hexAddress = normalize(address)
- var i = Object.keys(this.identities).length
- const oldNickname = this.configManager.nicknameForWallet(address)
- const name = oldNickname || `Account ${++i}`
- this.identities[hexAddress] = {
+ 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)
}
@@ -508,6 +358,7 @@ module.exports = class KeyringController extends EventEmitter {
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()])
@@ -523,7 +374,7 @@ module.exports = class KeyringController extends EventEmitter {
return this.encryptor.encrypt(this.password, serializedKeyrings)
})
.then((encryptedString) => {
- this.configManager.setVault(encryptedString)
+ this.store.updateState({ vault: encryptedString })
return true
})
}
@@ -536,7 +387,7 @@ module.exports = class KeyringController extends EventEmitter {
// Attempts to unlock the persisted encrypted storage,
// initializing the persisted keyrings to RAM.
unlockKeyrings (password) {
- const encryptedVault = this.configManager.getVault()
+ const encryptedVault = this.store.getState().vault
if (!encryptedVault) {
throw new Error('Cannot unlock without a previous vault.')
}
@@ -544,6 +395,7 @@ module.exports = class KeyringController extends EventEmitter {
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
})
@@ -589,6 +441,10 @@ module.exports = class KeyringController extends EventEmitter {
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 ] )
//
@@ -612,7 +468,7 @@ module.exports = class KeyringController extends EventEmitter {
// Returns the currently initialized keyring that manages
// the specified `address` if one exists.
getKeyringForAccount (address) {
- const hexed = normalize(address)
+ const hexed = normalizeAddress(address)
return Promise.all(this.keyrings.map((keyring) => {
return Promise.all([
@@ -621,7 +477,7 @@ module.exports = class KeyringController extends EventEmitter {
])
}))
.then(filter((candidate) => {
- const accounts = candidate[1].map(normalize)
+ const accounts = candidate[1].map(normalizeAddress)
return accounts.includes(hexed)
}))
.then((winners) => {
@@ -669,7 +525,7 @@ module.exports = class KeyringController extends EventEmitter {
clearKeyrings () {
let accounts
try {
- accounts = Object.keys(this.ethStore._currentState.accounts)
+ accounts = Object.keys(this.ethStore.getState())
} catch (e) {
accounts = []
}
@@ -677,12 +533,21 @@ module.exports = class KeyringController extends EventEmitter {
this.ethStore.removeAccount(address)
})
+ // clear keyrings from memory
this.keyrings = []
- this.identities = {}
- this.configManager.setSelectedAccount()
+ this.memStore.updateState({
+ keyrings: [],
+ identities: {},
+ })
}
-}
+ _updateMemStoreKeyrings() {
+ Promise.all(this.keyrings.map(this.displayForKeyring))
+ .then((keyrings) => {
+ this.memStore.updateState({ keyrings })
+ })
+ }
+}
-function noop () {}
+module.exports = KeyringController
diff --git a/app/scripts/keyrings/hd.js b/app/scripts/keyrings/hd.js
index 1b9796e07..2e3b74192 100644
--- a/app/scripts/keyrings/hd.js
+++ b/app/scripts/keyrings/hd.js
@@ -74,12 +74,13 @@ class HdKeyring extends EventEmitter {
}
// For eth_sign, we need to sign transactions:
- signMessage (withAccount, data) {
+ signMessage (withAccount, msgHex) {
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))
+ 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)
}
diff --git a/app/scripts/keyrings/simple.js b/app/scripts/keyrings/simple.js
index 46687fcaf..fa8e9fd78 100644
--- a/app/scripts/keyrings/simple.js
+++ b/app/scripts/keyrings/simple.js
@@ -58,12 +58,13 @@ class SimpleKeyring extends EventEmitter {
}
// For eth_sign, we need to sign transactions:
- signMessage (withAccount, data) {
+ signMessage (withAccount, msgHex) {
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))
+ 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)
}
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index e927c78ec..7ae2d4400 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -1,6 +1,4 @@
-const Migrator = require('pojo-migrator')
const MetamaskConfig = require('../config.js')
-const migrations = require('./migrations')
const ethUtil = require('ethereumjs-util')
const normalize = require('./sig-util').normalize
@@ -19,50 +17,19 @@ module.exports = ConfigManager
function ConfigManager (opts) {
// 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) {
@@ -96,15 +63,15 @@ 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)
}
@@ -121,11 +88,11 @@ ConfigManager.prototype.getVault = function () {
}
ConfigManager.prototype.getKeychains = function () {
- return this.migrator.getData().keychains || []
+ return this.getData().keychains || []
}
ConfigManager.prototype.setKeychains = function (keychains) {
- var data = this.migrator.getData()
+ var data = this.getData()
data.keychains = keychains
this.setData(data)
}
@@ -142,19 +109,19 @@ ConfigManager.prototype.setSelectedAccount = function (address) {
}
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
}
@@ -166,7 +133,7 @@ ConfigManager.prototype.setSeedWords = function (words) {
ConfigManager.prototype.getSeedWords = function () {
var data = this.getData()
- return ('seedWords' in data) && data.seedWords
+ return data.seedWords
}
ConfigManager.prototype.getCurrentRpcAddress = function () {
@@ -188,16 +155,12 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
}
}
-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 {
@@ -206,7 +169,7 @@ ConfigManager.prototype.getTxList = function () {
}
ConfigManager.prototype.setTxList = function (txList) {
- var data = this.migrator.getData()
+ var data = this.getData()
data.transactions = txList
this.setData(data)
}
@@ -239,7 +202,7 @@ ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
ConfigManager.prototype.getSalt = function () {
var data = this.getData()
- return ('salt' in data) && data.salt
+ return data.salt
}
ConfigManager.prototype.setSalt = function (salt) {
@@ -273,7 +236,7 @@ ConfigManager.prototype.setConfirmedDisclaimer = function (confirmed) {
ConfigManager.prototype.getConfirmedDisclaimer = function () {
var data = this.getData()
- return ('isDisclaimerConfirmed' in data) && data.isDisclaimerConfirmed
+ return data.isDisclaimerConfirmed
}
ConfigManager.prototype.setTOSHash = function (hash) {
@@ -284,93 +247,12 @@ ConfigManager.prototype.setTOSHash = function (hash) {
ConfigManager.prototype.getTOSHash = function () {
var data = this.getData()
- return ('TOSHash' in data) && data.TOSHash
-}
-
-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 fetch(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`)
- .then(response => response.json())
- .then((parsedResponse) => {
- this.setConversionPrice(parsedResponse.ticker.price)
- this.setConversionDate(parsedResponse.timestamp)
- }).catch((err) => {
- console.warn('MetaMask - Failed to query currency conversion.')
- this.setConversionPrice(0)
- this.setConversionDate('N/A')
- })
-}
-
-ConfigManager.prototype.setConversionPrice = function (price) {
- var data = this.getData()
- data.conversionRate = Number(price)
- this.setData(data)
-}
-
-ConfigManager.prototype.setConversionDate = function (datestring) {
- var data = this.getData()
- data.conversionDate = datestring
- this.setData(data)
-}
-
-ConfigManager.prototype.getConversionRate = 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.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.TOSHash
}
ConfigManager.prototype.getGasMultiplier = function () {
var data = this.getData()
- return ('gasMultiplier' in data) && data.gasMultiplier
+ return data.gasMultiplier
}
ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) {
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
index 7e2caf884..8812a507b 100644
--- a/app/scripts/lib/eth-store.js
+++ b/app/scripts/lib/eth-store.js
@@ -7,140 +7,126 @@
* on each new block.
*/
-const EventEmitter = require('events').EventEmitter
-const inherits = require('util').inherits
const async = require('async')
-const clone = require('clone')
const EthQuery = require('eth-query')
-
-module.exports = EthereumStore
+const ObservableStore = require('obs-store')
+function noop() {}
-inherits(EthereumStore, EventEmitter)
-function EthereumStore(engine) {
- const self = this
- EventEmitter.call(self)
- self._currentState = {
- accounts: {},
- transactions: {},
+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
}
- self._query = new EthQuery(engine)
-
- engine.on('block', self._updateForBlock.bind(self))
-}
-
-//
-// public
-//
-
-EthereumStore.prototype.getState = function () {
- const self = this
- return clone(self._currentState)
-}
-EthereumStore.prototype.addAccount = function (address) {
- const self = this
- self._currentState.accounts[address] = {}
- self._didUpdate()
- if (!self.currentBlockNumber) return
- self._updateAccount(address, () => {
- self._didUpdate()
- })
-}
+ //
+ // public
+ //
-EthereumStore.prototype.removeAccount = function (address) {
- const self = this
- delete self._currentState.accounts[address]
- self._didUpdate()
-}
+ addAccount (address) {
+ const accounts = this.getState().accounts
+ accounts[address] = {}
+ this.updateState({ accounts })
+ if (!this._currentBlockNumber) return
+ this._updateAccount(address)
+ }
-EthereumStore.prototype.addTransaction = function (txHash) {
- const self = this
- self._currentState.transactions[txHash] = {}
- self._didUpdate()
- if (!self.currentBlockNumber) return
- self._updateTransaction(self.currentBlockNumber, txHash, noop)
-}
+ removeAccount (address) {
+ const accounts = this.getState().accounts
+ delete accounts[address]
+ this.updateState({ accounts })
+ }
-EthereumStore.prototype.removeTransaction = function (address) {
- const self = this
- delete self._currentState.transactions[address]
- self._didUpdate()
-}
+ 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
-//
-EthereumStore.prototype._didUpdate = function () {
- const self = this
- var state = self.getState()
- self.emit('update', state)
-}
+ //
+ // 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())
+ })
+ }
-EthereumStore.prototype._updateForBlock = function (block) {
- const self = this
- var blockNumber = '0x' + block.number.toString('hex')
- self.currentBlockNumber = blockNumber
- async.parallel([
- self._updateAccounts.bind(self),
- self._updateTransactions.bind(self, blockNumber),
- ], function (err) {
- if (err) return console.error(err)
- self.emit('block', self.getState())
- self._didUpdate()
- })
-}
+ _updateAccounts (cb = noop) {
+ const accounts = this.getState().accounts
+ const addresses = Object.keys(accounts)
+ async.each(addresses, this._updateAccount.bind(this), cb)
+ }
-EthereumStore.prototype._updateAccounts = function (cb) {
- var accountsState = this._currentState.accounts
- var addresses = Object.keys(accountsState)
- 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)
+ })
+ }
-EthereumStore.prototype._updateAccount = function (address, cb) {
- var accountsState = this._currentState.accounts
- this.getAccount(address, function (err, result) {
- if (err) return cb(err)
- result.address = address
- // only populate if the entry is still present
- if (accountsState[address]) {
- accountsState[address] = result
- }
- 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)
+ }
-EthereumStore.prototype.getAccount = function (address, cb) {
- 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)
-}
+ _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)
+ })
+ }
-EthereumStore.prototype._updateTransactions = function (block, cb) {
- const self = this
- var transactionsState = self._currentState.transactions
- var txHashes = Object.keys(transactionsState)
- async.each(txHashes, self._updateTransaction.bind(self, block), cb)
-}
+ _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)
+ }
-EthereumStore.prototype._updateTransaction = function (block, txHash, cb) {
- const self = this
- // would use the block here to determine how many confirmations the tx has
- var transactionsState = self._currentState.transactions
- self._query.getTransaction(txHash, function (err, result) {
- if (err) return cb(err)
- // only populate if the entry is still present
- if (transactionsState[txHash]) {
- transactionsState[txHash] = result
- self._didUpdate()
- }
- cb(null, result)
- })
}
-function noop() {}
+module.exports = EthereumStore \ No newline at end of file
diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js
index e4cbca456..1afe5f651 100644
--- a/app/scripts/lib/idStore.js
+++ b/app/scripts/lib/idStore.js
@@ -96,10 +96,6 @@ IdentityStore.prototype.getState = function () {
seedWords: seedWords,
isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(),
selectedAddress: configManager.getSelectedAccount(),
- shapeShiftTxList: configManager.getShapeShiftTxList(),
- currentFiat: configManager.getCurrentFiat(),
- conversionRate: configManager.getConversionRate(),
- conversionDate: configManager.getConversionDate(),
gasMultiplier: configManager.getGasMultiplier(),
}))
}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index 11bd5cc3a..faecac137 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -1,7 +1,7 @@
-const Streams = require('mississippi')
+const pipe = require('pump')
const StreamProvider = require('web3-stream-provider')
+const LocalStorageStore = require('obs-store')
const ObjectMultiplex = require('./obj-multiplex')
-const RemoteStore = require('./remote-store.js').RemoteStore
const createRandomId = require('./random-id')
module.exports = MetamaskInpageProvider
@@ -10,33 +10,30 @@ function MetamaskInpageProvider (connectionStream) {
const self = this
// setup connectionStream multiplexing
- var multiStream = ObjectMultiplex()
- Streams.pipe(connectionStream, multiStream, connectionStream, function (err) {
- let warningMsg = 'MetamaskInpageProvider - lost connection to MetaMask'
- if (err) warningMsg += '\n' + err.stack
- console.warn(warningMsg)
- })
- 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) {
- let warningMsg = 'MetamaskInpageProvider - lost connection to MetaMask publicConfig'
- if (err) warningMsg += '\n' + err.stack
- console.warn(warningMsg)
- })
- 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) {
- let warningMsg = 'MetamaskInpageProvider - lost connection to MetaMask provider'
- if (err) warningMsg += '\n' + err.stack
- console.warn(warningMsg)
- })
- 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
@@ -66,20 +63,20 @@ function MetamaskInpageProvider (connectionStream) {
MetamaskInpageProvider.prototype.send = function (payload) {
const self = this
- let selectedAccount
+ let selectedAddress
let result = null
switch (payload.method) {
case 'eth_accounts':
// read from localStorage
- selectedAccount = self.publicConfigStore.get('selectedAccount')
- result = selectedAccount ? [selectedAccount] : []
+ selectedAddress = self.publicConfigStore.getState().selectedAddress
+ result = selectedAddress ? [selectedAddress] : []
break
case 'eth_coinbase':
// read from localStorage
- selectedAccount = self.publicConfigStore.get('selectedAccount')
- result = selectedAccount || '0x0000000000000000000000000000000000000000'
+ selectedAddress = self.publicConfigStore.getState().selectedAddress
+ result = selectedAddress
break
case 'eth_uninstallFilter':
@@ -115,18 +112,6 @@ MetamaskInpageProvider.prototype.isMetaMask = 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
-}
-
function eachJsonMessage (payload, transformFn) {
if (Array.isArray(payload)) {
return payload.map(transformFn)
@@ -135,4 +120,10 @@ function eachJsonMessage (payload, transformFn) {
}
}
+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/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/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/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/metamask-controller.js b/app/scripts/metamask-controller.js
index 629216e42..fb2040c63 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1,248 +1,405 @@
const EventEmitter = require('events')
const extend = require('xtend')
+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 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 messageManager = require('./lib/message-manager')
+const ShapeShiftController = require('./lib/controllers/shapeshift')
+const MessageManager = require('./lib/message-manager')
const TxManager = require('./transaction-manager')
-const HostStore = require('./lib/remote-store.js').HostStore
-const Web3 = require('web3')
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')
+
const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter {
constructor (opts) {
super()
- this.state = { network: 'loading' }
this.opts = opts
- this.configManager = new ConfigManager(opts)
+ 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({
- configManager: this.configManager,
- getNetwork: this.getStateNetwork.bind(this),
+ initState: initState.KeyringController,
+ ethStore: this.ethStore,
+ getNetwork: this.getNetworkState.bind(this),
})
- // notices
- this.noticeController = new NoticeController({
- configManager: this.configManager,
+ this.keyringController.on('newAccount', (address) => {
+ this.preferencesController.setSelectedAddress(address)
+ autoFaucet(address)
})
- this.noticeController.updateNoticesList()
- // to be uncommented when retrieving notices from a remote server.
- // this.noticeController.startPolling()
- this.provider = this.initializeProvider(opts)
- this.ethStore = new EthStore(this.provider)
- this.keyringController.setStore(this.ethStore)
- this.getNetwork()
- this.messageManager = messageManager
+
+ // tx mgmt
this.txManager = new TxManager({
- txList: this.configManager.getTxList(),
+ initState: initState.TransactionManager,
+ networkStore: this.networkStore,
+ preferencesStore: this.preferencesController.store,
txHistoryLimit: 40,
- setTxList: this.configManager.setTxList.bind(this.configManager),
- getSelectedAccount: this.configManager.getSelectedAccount.bind(this.configManager),
- getGasMultiplier: this.configManager.getGasMultiplier.bind(this.configManager),
- getNetwork: this.getStateNetwork.bind(this),
+ getNetwork: this.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider,
blockTracker: this.provider,
})
- this.publicConfigStore = this.initPublicConfigStore()
- var currentFiat = this.configManager.getCurrentFiat() || 'USD'
- this.configManager.setCurrentFiat(currentFiat)
- this.configManager.updateConversionRate()
+ // notices
+ this.noticeController = new NoticeController({
+ initState: initState.NoticeController,
+ })
+ this.noticeController.updateNoticesList()
+ // to be uncommented when retrieving notices from a remote server.
+ // this.noticeController.startPolling()
- this.checkTOSChange()
+ this.shapeshiftController = new ShapeShiftController({
+ initState: initState.ShapeShiftController,
+ })
+
+ this.lookupNetwork()
+ this.messageManager = new MessageManager()
+ this.publicConfigStore = this.initPublicConfigStore()
- this.scheduleConversionInterval()
+ this.checkTOSChange()
// TEMPORARY UNTIL FULL DEPRECATION:
this.idStoreMigrator = new IdStoreMigrator({
configManager: this.configManager,
})
- this.ethStore.on('update', this.sendUpdate.bind(this))
- this.keyringController.on('update', this.sendUpdate.bind(this))
- this.txManager.on('update', this.sendUpdate.bind(this))
+ // 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))
}
- getState () {
- return this.keyringController.getState()
- .then((keyringControllerState) => {
- return extend(
- this.state,
- this.ethStore.getState(),
- this.configManager.getConfig(),
- this.txManager.getState(),
- keyringControllerState,
- this.noticeController.getState(), {
- lostAccounts: this.configManager.getLostAccounts(),
- }
- )
+ //
+ // 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),
+ publicConfigStore
+ )
+
+ function selectPublicState(state) {
+ const result = { selectedAddress: undefined }
+ try {
+ result.selectedAddress = state.PreferencesController.selectedAddress
+ } 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.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(),
+ isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(),
+ seedWords: this.configManager.getSeedWords(),
+ }
+ )
+ }
+
+ //
+ // Remote Features
+ //
+
getApi () {
const keyringController = this.keyringController
+ const preferencesController = this.preferencesController
const txManager = this.txManager
+ const messageManager = this.messageManager
const noticeController = this.noticeController
return {
- getState: nodeify(this.getState.bind(this)),
- setRpcTarget: this.setRpcTarget.bind(this),
- setProviderType: this.setProviderType.bind(this),
- useEtherscanProvider: this.useEtherscanProvider.bind(this),
- agreeToDisclaimer: this.agreeToDisclaimer.bind(this),
- resetDisclaimer: this.resetDisclaimer.bind(this),
- setCurrentFiat: this.setCurrentFiat.bind(this),
- setTOSHash: this.setTOSHash.bind(this),
- checkTOSChange: this.checkTOSChange.bind(this),
- setGasMultiplier: this.setGasMultiplier.bind(this),
- markAccountsFound: this.markAccountsFound.bind(this),
-
- // forward directly to keyringController
- createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
- createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController),
- placeSeedWords: nodeify(keyringController.placeSeedWords).bind(keyringController),
- clearSeedWordCache: nodeify(keyringController.clearSeedWordCache).bind(keyringController),
- setLocked: nodeify(keyringController.setLocked).bind(keyringController),
- submitPassword: (password, cb) => {
- this.migrateOldVaultIfAny(password)
- .then(keyringController.submitPassword.bind(keyringController, password))
- .then((newState) => { cb(null, newState) })
- .catch((reason) => { cb(reason) })
- },
- addNewKeyring: (type, opts, cb) => {
- keyringController.addNewKeyring(type, opts)
- .then(() => keyringController.fullUpdate())
- .then((newState) => { cb(null, newState) })
- .catch((reason) => { cb(reason) })
- },
- addNewAccount: nodeify(keyringController.addNewAccount).bind(keyringController),
- setSelectedAccount: nodeify(keyringController.setSelectedAccount).bind(keyringController),
- saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
- exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
-
- // signing methods
- approveTransaction: txManager.approveTransaction.bind(txManager),
- cancelTransaction: txManager.cancelTransaction.bind(txManager),
- signMessage: keyringController.signMessage.bind(keyringController),
- cancelMessage: keyringController.cancelMessage.bind(keyringController),
-
+ // etc
+ 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),
+ resetDisclaimer: this.resetDisclaimer.bind(this),
+ setCurrentCurrency: this.setCurrentCurrency.bind(this),
+ setTOSHash: this.setTOSHash.bind(this),
+ checkTOSChange: this.checkTOSChange.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),
+ 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) {
- // 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)
- }
- }
+ setupTrustedCommunication (connectionStream, originDomain) {
+ // setup multiplexing
+ var mx = setupMultiplex(connectionStream)
+ // connect features
+ this.setupControllerConnection(mx.createStream('controller'))
+ this.setupProviderConnection(mx.createStream('provider'), originDomain)
+ }
+
+ 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)
}
}
}
+ setupPublicConfig (outStream) {
+ pipe(
+ this.publicConfigStore,
+ outStream
+ )
+ }
+
sendUpdate () {
- this.getState()
- .then((state) => {
- this.emit('update', state)
- })
+ this.emit('update', this.getState())
}
- initializeProvider (opts) {
- const keyringController = this.keyringController
+ //
+ // Vault Management
+ //
- var providerOpts = {
- static: {
- eth_syncing: false,
- web3_clientVersion: `MetaMask/v${version}`,
- },
- rpcUrl: this.configManager.getCurrentRpcAddress(),
- // account mgmt
- getAccounts: (cb) => {
- var selectedAccount = this.configManager.getSelectedAccount()
- var result = selectedAccount ? [selectedAccount] : []
- cb(null, result)
- },
- // tx signing
- processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb),
- // msg signing
- approveMessage: this.newUnsignedMessage.bind(this),
- signMessage: (...args) => {
- keyringController.signMessage(...args)
- this.sendUpdate()
- },
- }
+ submitPassword (password, cb) {
+ this.migrateOldVaultIfAny(password)
+ .then(this.keyringController.submitPassword.bind(this.keyringController, password))
+ .then((newState) => { cb(null, newState) })
+ .catch((reason) => { cb(reason) })
+ }
- var provider = MetaMaskProvider(providerOpts)
- var web3 = new Web3(provider)
- this.web3 = web3
- keyringController.web3 = web3
- provider.on('block', this.processBlock.bind(this))
- provider.on('error', this.getNetwork.bind(this))
+ //
+ // 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 = configToPublic(this.configManager.getConfig())
- var publicConfigStore = new HostStore(initPublicState)
-
- // subscribe to changes
- this.configManager.subscribe(function (state) {
- storeSetFromObj(publicConfigStore, configToPublic(state))
+ // 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()
})
+ }
- this.keyringController.on('newAccount', (account) => {
- autoFaucet(account)
+ // 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())
+ }
+
+ 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) })
+ }
- // config substate
- function configToPublic (state) {
- return {
- selectedAccount: state.selectedAccount,
- }
- }
- // dump obj into store
- function storeSetFromObj (store, obj) {
- Object.keys(obj).forEach(function (key) {
- store.set(key, obj[key])
- })
- }
- return publicConfigStore
- }
+ //
+ // Identity Management
+ //
newUnapprovedTransaction (txParams, cb) {
const self = this
@@ -265,66 +422,105 @@ module.exports = class MetamaskController extends EventEmitter {
}
newUnsignedMessage (msgParams, cb) {
- var state = this.keyringController.getState()
- if (!state.isUnlocked) {
- this.keyringController.addUnconfirmedMessage(msgParams, cb)
- this.opts.unlockAccountMessage()
- } else {
- this.addUnconfirmedMessage(msgParams, cb)
- this.sendUpdate()
- }
+ 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)}`))
+ }
+ })
}
- addUnconfirmedMessage (msgParams, cb) {
- const keyringController = this.keyringController
- const msgId = keyringController.addUnconfirmedMessage(msgParams, cb)
- this.opts.showUnconfirmedMessage(msgParams, msgId)
+ 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)
}
- setupPublicConfig (stream) {
- var storeStream = this.publicConfigStore.createStream()
- stream.pipe(storeStream).pipe(stream)
+
+ markAccountsFound (cb) {
+ this.configManager.setLostAccounts([])
+ this.sendUpdate()
+ cb(null, this.getState())
}
- // Log blocks
- processBlock (block) {
- if (global.METAMASK_DEBUG) {
- console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
+ // 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)
}
- this.verifyNetwork()
+
+ 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)
}
- verifyNetwork () {
- // Check network when restoring connectivity:
- if (this.state.network === 'loading') {
- this.getNetwork()
- }
+ checkIfShouldMigrate() {
+ return !!this.configManager.getWallet() && !this.configManager.getVault()
}
- // config
- //
+ restoreOldVaultAccounts(migratorOutput) {
+ const { serialized } = migratorOutput
+ return this.keyringController.restoreKeyring(serialized)
+ .then(() => migratorOutput)
+ }
- setTOSHash (hash) {
- try {
- this.configManager.setTOSHash(hash)
- } catch (err) {
- console.error('Error in setting terms of service hash.')
+ restoreOldLostAccounts(migratorOutput) {
+ const { lostAccounts } = migratorOutput
+ if (lostAccounts) {
+ this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
+ return this.importLostAccounts(migratorOutput)
}
+ return Promise.resolve(migratorOutput)
}
- checkTOSChange () {
- try {
- const storedHash = this.configManager.getTOSHash() || 0
- if (storedHash !== global.TOS_HASH) {
- this.resetDisclaimer()
- this.setTOSHash(global.TOS_HASH)
- }
- } catch (err) {
- console.error('Error in checking TOS change.')
- }
+ // 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,
+ })
}
+ //
// disclaimer
+ //
agreeToDisclaimer (cb) {
try {
@@ -343,159 +539,138 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
- setCurrentFiat (fiat, cb) {
+ setTOSHash (hash) {
try {
- this.configManager.setCurrentFiat(fiat)
- this.configManager.updateConversionRate()
- this.scheduleConversionInterval()
- const data = {
- conversionRate: this.configManager.getConversionRate(),
- currentFiat: this.configManager.getCurrentFiat(),
- conversionDate: this.configManager.getConversionDate(),
- }
- cb(data)
+ this.configManager.setTOSHash(hash)
} catch (err) {
- cb(null, err)
+ console.error('Error in setting terms of service hash.')
}
}
- scheduleConversionInterval () {
- if (this.conversionInterval) {
- clearInterval(this.conversionInterval)
+ checkTOSChange () {
+ try {
+ const storedHash = this.configManager.getTOSHash() || 0
+ if (storedHash !== global.TOS_HASH) {
+ this.resetDisclaimer()
+ this.setTOSHash(global.TOS_HASH)
+ }
+ } catch (err) {
+ console.error('Error in checking TOS change.')
}
- this.conversionInterval = setInterval(() => {
- this.configManager.updateConversionRate()
- }, 300000)
}
- // called from popup
- setRpcTarget (rpcTarget) {
- this.configManager.setRpcTarget(rpcTarget)
- extension.runtime.reload()
- this.getNetwork()
- }
+ //
+ // config
+ //
- setProviderType (type) {
- this.configManager.setProviderType(type)
- extension.runtime.reload()
- this.getNetwork()
+ // Log blocks
+ logBlock (block) {
+ if (global.METAMASK_DEBUG) {
+ console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
+ }
+ this.verifyNetwork()
}
- useEtherscanProvider () {
- this.configManager.useEtherscanProvider()
- extension.runtime.reload()
+ setCurrentCurrency (currencyCode, cb) {
+ try {
+ this.currencyController.setCurrentCurrency(currencyCode)
+ this.currencyController.updateConversionRate()
+ const data = {
+ conversionRate: this.currencyController.getConversionRate(),
+ currentFiat: this.currencyController.getCurrentCurrency(),
+ conversionDate: this.currencyController.getConversionDate(),
+ }
+ cb(null, data)
+ } catch (err) {
+ cb(err)
+ }
}
buyEth (address, amount) {
if (!amount) amount = '5'
- var network = this.state.network
- var url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
+ const network = this.getNetworkState()
+ let url
- if (network === '3') {
- url = 'https://faucet.metamask.io/'
+ 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
}
- extension.tabs.create({
- url,
- })
+ if (url) extension.tabs.create({ url })
}
createShapeShiftTx (depositAddress, depositType) {
- this.configManager.createShapeShiftTx(depositAddress, depositType)
- }
-
- getNetwork (err) {
- if (err) {
- this.state.network = 'loading'
- this.sendUpdate()
- }
-
- this.web3.version.getNetwork((err, network) => {
- if (err) {
- this.state.network = 'loading'
- return this.sendUpdate()
- }
- if (global.METAMASK_DEBUG) {
- console.log('web3.getNetwork returned ' + network)
- }
- this.state.network = network
- this.sendUpdate()
- })
+ this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
}
setGasMultiplier (gasMultiplier, cb) {
try {
- this.configManager.setGasMultiplier(gasMultiplier)
+ this.txManager.setGasMultiplier(gasMultiplier)
cb()
} catch (err) {
cb(err)
}
}
- getStateNetwork () {
- return this.state.network
- }
+ //
+ // network
+ //
- markAccountsFound (cb) {
- this.configManager.setLostAccounts([])
- this.sendUpdate()
- cb(null, this.getState())
+ verifyNetwork () {
+ // Check network when restoring connectivity:
+ if (this.isNetworkLoading()) this.lookupNetwork()
}
- // 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) {
+ setRpcTarget (rpcTarget) {
+ this.configManager.setRpcTarget(rpcTarget)
+ extension.runtime.reload()
+ this.lookupNetwork()
+ }
- if (!this.checkIfShouldMigrate()) {
- return Promise.resolve(password)
- }
+ setProviderType (type) {
+ this.configManager.setProviderType(type)
+ extension.runtime.reload()
+ this.lookupNetwork()
+ }
- const keyringController = this.keyringController
+ useEtherscanProvider () {
+ this.configManager.useEtherscanProvider()
+ extension.runtime.reload()
+ }
- return this.idStoreMigrator.migratedVaultForPassword(password)
- .then(this.restoreOldVaultAccounts.bind(this))
- .then(this.restoreOldLostAccounts.bind(this))
- .then(keyringController.persistAllKeyrings.bind(keyringController, password))
- .then(() => password)
+ getNetworkState () {
+ return this.networkStore.getState().network
}
- checkIfShouldMigrate() {
- return !!this.configManager.getWallet() && !this.configManager.getVault()
+ setNetworkState (network) {
+ return this.networkStore.updateState({ network })
}
- restoreOldVaultAccounts(migratorOutput) {
- const { serialized } = migratorOutput
- return this.keyringController.restoreKeyring(serialized)
- .then(() => migratorOutput)
+ isNetworkLoading () {
+ return this.getNetworkState() === 'loading'
}
- restoreOldLostAccounts(migratorOutput) {
- const { lostAccounts } = migratorOutput
- if (lostAccounts) {
- this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
- return this.importLostAccounts(migratorOutput)
+ lookupNetwork (err) {
+ if (err) {
+ this.setNetworkState('loading')
}
- return Promise.resolve(migratorOutput)
- }
- // 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,
+ 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)
})
}
+
}
diff --git a/app/scripts/migrations/002.js b/app/scripts/migrations/002.js
index 0b654f825..476b0a43a 100644
--- a/app/scripts/migrations/002.js
+++ b/app/scripts/migrations/002.js
@@ -1,13 +1,16 @@
+const version = 2
+
module.exports = {
- version: 2,
+ version,
- migrate: function (data) {
+ migrate: function (versionedData) {
+ 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..eceaeaa4b 100644
--- a/app/scripts/migrations/003.js
+++ b/app/scripts/migrations/003.js
@@ -1,15 +1,17 @@
-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/'
module.exports = {
- version: 3,
+ version,
- migrate: function (data) {
+ migrate: function (versionedData) {
+ 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..0f9850208 100644
--- a/app/scripts/migrations/004.js
+++ b/app/scripts/migrations/004.js
@@ -1,22 +1,25 @@
+const version = 4
+
module.exports = {
- version: 4,
+ version,
- migrate: function (data) {
+ migrate: function (versionedData) {
+ versionedData.meta.version = version
try {
- if (data.config.provider.type !== 'rpc') return data
- switch (data.config.provider.rpcTarget) {
+ if (versionedData.data.config.provider.type !== 'rpc') return Promise.resolve(versionedData)
+ switch (versionedData.data.config.provider.rpcTarget) {
case 'https://testrpc.metamask.io/':
- data.config.provider = {
+ versionedData.data.config.provider = {
type: 'testnet',
}
break
case 'https://rpc.metamask.io/':
- data.config.provider = {
+ versionedData.data.config.provider = {
type: 'mainnet',
}
break
}
} catch (_) {}
- return data
+ return Promise.resolve(versionedData)
},
}
diff --git a/app/scripts/migrations/005.js b/app/scripts/migrations/005.js
new file mode 100644
index 000000000..65f62a861
--- /dev/null
+++ b/app/scripts/migrations/005.js
@@ -0,0 +1,41 @@
+const version = 5
+
+/*
+
+This migration moves state from the flat state trie into KeyringController substate
+
+*/
+
+const extend = require('xtend')
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ 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..950c4deb8
--- /dev/null
+++ b/app/scripts/migrations/006.js
@@ -0,0 +1,41 @@
+const version = 6
+
+/*
+
+This migration moves KeyringController.selectedAddress to PreferencesController.selectedAddress
+
+*/
+
+const extend = require('xtend')
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ 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..3ae8cdc2d
--- /dev/null
+++ b/app/scripts/migrations/007.js
@@ -0,0 +1,38 @@
+const version = 7
+
+/*
+
+This migration breaks out the TransactionManager substate
+
+*/
+
+const extend = require('xtend')
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ 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..7f6e72ee6
--- /dev/null
+++ b/app/scripts/migrations/008.js
@@ -0,0 +1,36 @@
+const version = 8
+
+/*
+
+This migration breaks out the NoticeController substate
+
+*/
+
+const extend = require('xtend')
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ 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..38e6dcc09
--- /dev/null
+++ b/app/scripts/migrations/009.js
@@ -0,0 +1,40 @@
+const version = 9
+
+/*
+
+This migration breaks out the CurrencyController substate
+
+*/
+
+const merge = require('deep-extend')
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ 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 || 'USD',
+ conversionRate: state.conversionRate,
+ conversionDate: state.conversionDate,
+ },
+ })
+ delete newState.currentFiat
+ 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..d41c63fcd
--- /dev/null
+++ b/app/scripts/migrations/010.js
@@ -0,0 +1,36 @@
+const version = 10
+
+/*
+
+This migration breaks out the CurrencyController substate
+
+*/
+
+const merge = require('deep-extend')
+
+module.exports = {
+ version,
+
+ migrate: function (versionedData) {
+ 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/_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..2db8646b0
--- /dev/null
+++ b/app/scripts/migrations/index.js
@@ -0,0 +1,24 @@
+/* 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'),
+]
diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js
index 00c87c670..ba7c68df4 100644
--- a/app/scripts/notice-controller.js
+++ b/app/scripts/notice-controller.js
@@ -1,36 +1,37 @@
const EventEmitter = require('events').EventEmitter
-const hardCodedNotices = require('../../development/notices.json')
+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.configManager = opts.configManager
this.noticePoller = null
+ const initState = extend({
+ noticesList: [],
+ }, opts.initState)
+ this.store = new ObservableStore(initState)
+ this.memStore = new ObservableStore({})
+ this.store.subscribe(() => this._updateMemstore())
}
- getState () {
- var lastUnreadNotice = this.getLatestUnreadNotice()
+ getNoticesList () {
+ return this.store.getState().noticesList
+ }
- return {
- lastUnreadNotice: lastUnreadNotice,
- noActiveNotices: !lastUnreadNotice,
- }
+ getUnreadNotices () {
+ const notices = this.getNoticesList()
+ return notices.filter((notice) => notice.read === false)
}
- getNoticesList () {
- var data = this.configManager.getData()
- if ('noticesList' in data) {
- return data.noticesList
- } else {
- return []
- }
+ getLatestUnreadNotice () {
+ const unreadNotices = this.getUnreadNotices()
+ return unreadNotices[unreadNotices.length - 1]
}
- setNoticesList (list) {
- var data = this.configManager.getData()
- data.noticesList = list
- this.configManager.setData(data)
+ setNoticesList (noticesList) {
+ this.store.updateState({ noticesList })
return Promise.resolve(true)
}
@@ -56,14 +57,6 @@ module.exports = class NoticeController extends EventEmitter {
})
}
- getLatestUnreadNotice () {
- var notices = this.getNoticesList()
- var filteredNotices = notices.filter((notice) => {
- return notice.read === false
- })
- return filteredNotices[filteredNotices.length - 1]
- }
-
startPolling () {
if (this.noticePoller) {
clearInterval(this.noticePoller)
@@ -92,5 +85,10 @@ module.exports = class NoticeController extends EventEmitter {
return Promise.resolve(hardCodedNotices)
}
+ _updateMemstore () {
+ const lastUnreadNotice = this.getLatestUnreadNotice()
+ const noActiveNotices = !lastUnreadNotice
+ this.memStore.updateState({ lastUnreadNotice, noActiveNotices })
+ }
}
diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js
index 6d0121afd..6299091f2 100644
--- a/app/scripts/transaction-manager.js
+++ b/app/scripts/transaction-manager.js
@@ -2,6 +2,7 @@ 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')
@@ -10,33 +11,53 @@ const createId = require('./lib/random-id')
module.exports = class TransactionManager extends EventEmitter {
constructor (opts) {
super()
- this.txList = opts.txList || []
- this._setTxList = opts.setTxList
+ 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.getSelectedAccount = opts.getSelectedAccount
this.provider = opts.provider
this.blockTracker = opts.blockTracker
this.txProviderUtils = new TxProviderUtil(this.provider)
this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
- this.getGasMultiplier = opts.getGasMultiplier
- this.getNetwork = opts.getNetwork
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 () {
- var selectedAccount = this.getSelectedAccount()
- return {
- transactions: this.getTxList(),
- unconfTxs: this.getUnapprovedTxList(),
- selectedAccountTxList: this.getFilteredTxList({metamaskNetworkId: this.getNetwork(), from: selectedAccount}),
- }
+ return this.memStore.getState()
+ }
+
+ getNetwork () {
+ return this.networkStore.getState().network
}
-// Returns the tx list
+ getSelectedAddress () {
+ return this.preferencesStore.getState().selectedAddress
+ }
+
+ // Returns the tx list
getTxList () {
let network = this.getNetwork()
- return this.txList.filter(txMeta => txMeta.metamaskNetworkId === network)
+ 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
@@ -108,7 +129,7 @@ module.exports = class TransactionManager extends EventEmitter {
id: txId,
time: time,
status: 'unapproved',
- gasMultiplier: this.getGasMultiplier() || 1,
+ gasMultiplier: this.getGasMultiplier(),
metamaskNetworkId: this.getNetwork(),
txParams: txParams,
}
@@ -239,7 +260,7 @@ module.exports = class TransactionManager extends EventEmitter {
getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => {
- if (key in txMeta.txParams) {
+ if (txMeta.txParams[key]) {
return txMeta.txParams[key] === value
} else {
return txMeta[key] === value
@@ -351,9 +372,17 @@ module.exports = class TransactionManager extends EventEmitter {
// Saves the new/updated txList.
// Function is intended only for internal use
- _saveTxList (txList) {
- this.txList = txList
- this._setTxList(txList)
+ _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 })
}
}