diff options
-rw-r--r-- | app/scripts/background.js | 212 | ||||
-rw-r--r-- | app/scripts/lib/migrator/index.js | 31 | ||||
-rw-r--r-- | app/scripts/migrations/002.js | 15 | ||||
-rw-r--r-- | app/scripts/migrations/003.js | 16 | ||||
-rw-r--r-- | app/scripts/migrations/004.js | 17 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | test/unit/migrations-test.js | 48 |
7 files changed, 204 insertions, 136 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index 8aa886594..697417fd2 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,7 +1,8 @@ const urlUtil = require('url') const Dnode = require('dnode') const eos = require('end-of-stream') -const Migrator = require('pojo-migrator') +const asyncQ = require('async-q') +const Migrator = require('./lib/migrator/') const migrations = require('./lib/migrations') const LocalStorageStore = require('./lib/observable/local-storage') const PortStream = require('./lib/port-stream.js') @@ -16,101 +17,143 @@ const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' 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) }) // // State and Persistence // -// state persistence - -let dataStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) -// initial state for first time users -if (!dataStore.get()) { - dataStore.put({ meta: { version: 0 }, data: firstTimeState }) +function loadStateFromPersistence() { + // migrations + let migrator = new Migrator({ migrations }) + let initialState = { + meta: { version: migrator.defaultVersion }, + data: firstTimeState, + } + return asyncQ.waterfall([ + // read from disk + () => Promise.resolve(diskStore.get() || initialState), + // migrate data + (versionedData) => migrator.migrateData(versionedData), + // write to disk + (versionedData) => { + diskStore.put(versionedData) + return Promise.resolve(versionedData) + }, + // resolve to just data + (versionedData) => Promise.resolve(versionedData.data), + ]) } -// migrations +function setupController (initState) { -let migrator = new Migrator({ - migrations, - // Data persistence methods - loadData: () => dataStore.get(), - setData: (newState) => dataStore.put(newState), -}) + // + // MetaMask Controller + // -// -// MetaMask Controller -// + const controller = new MetamaskController({ + // User confirmation callbacks: + showUnconfirmedMessage: triggerUi, + unlockAccountMessage: triggerUi, + showUnapprovedTx: triggerUi, + // initial state + initState, + }) -const controller = new MetamaskController({ - // User confirmation callbacks: - showUnconfirmedMessage: triggerUi, - unlockAccountMessage: triggerUi, - showUnapprovedTx: triggerUi, - // initial state - initState: migrator.getData(), -}) -// setup state persistence -controller.store.subscribe((newState) => migrator.saveData(newState)) + // setup state persistence + controller.store.subscribe((newState) => diskStore) + + // + // 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 = remotePort.name === 'popup' + setupTrustedCommunication(portStream, 'MetaMask', remotePort.name) + } else { + // communication with page + var originDomain = urlUtil.parse(remotePort.sender.url).hostname + setupUntrustedCommunication(portStream, originDomain) + } + } -// -// connect to other contexts -// + function setupUntrustedCommunication (connectionStream, originDomain) { + // setup multiplexing + var mx = setupMultiplex(connectionStream) + // connect features + controller.setupProviderConnection(mx.createStream('provider'), originDomain) + controller.setupPublicConfig(mx.createStream('publicConfig')) + } -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 setupTrustedCommunication (connectionStream, originDomain) { + // setup multiplexing + var mx = setupMultiplex(connectionStream) + // connect features + setupControllerConnection(mx.createStream('controller')) + controller.setupProviderConnection(mx.createStream('provider'), originDomain) } -} -function setupUntrustedCommunication (connectionStream, originDomain) { - // setup multiplexing - var mx = setupMultiplex(connectionStream) - // connect features - controller.setupProviderConnection(mx.createStream('provider'), originDomain) - controller.setupPublicConfig(mx.createStream('publicConfig')) -} + // + // remote features + // + + 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 + }) + }) + } -function setupTrustedCommunication (connectionStream, originDomain) { - // setup multiplexing - var mx = setupMultiplex(connectionStream) - // connect features - setupControllerConnection(mx.createStream('controller')) - controller.setupProviderConnection(mx.createStream('provider'), originDomain) -} + // + // User Interface setup + // + + controller.txManager.on('updateBadge', updateBadge) + + // plugin badge text + 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) + } + extension.browserAction.setBadgeText({ text: label }) + extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' }) + } -// -// remote features -// + return Promise.resolve() -function setupControllerConnection (stream) { - controller.stream = stream - var api = controller.getApi() - var dnode = Dnode(api) - stream.pipe(dnode).pipe(stream) - dnode.on('remote', (remote) => { - // push updates to popup - var sendUpdate = remote.sendUpdate.bind(remote) - controller.on('update', sendUpdate) - // teardown on disconnect - eos(stream, () => { - controller.removeListener('update', sendUpdate) - popupIsOpen = false - }) - }) } // -// User Interface setup +// Etc... // // popup trigger @@ -123,19 +166,4 @@ extension.runtime.onInstalled.addListener(function (details) { if ((details.reason === 'install') && (!METAMASK_DEBUG)) { extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) } -}) - -// plugin badge text -controller.txManager.on('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) - } - extension.browserAction.setBadgeText({ text: label }) - extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' }) -} +})
\ No newline at end of file diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js new file mode 100644 index 000000000..02d8c2335 --- /dev/null +++ b/app/scripts/lib/migrator/index.js @@ -0,0 +1,31 @@ +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 (meta = { version: this.defaultVersion }) { + let remaining = this.migrations.filter(migrationIsPending) + + return ( + asyncQ.eachSeries(remaining, (migration) => migration.migrate(meta)) + .then(() => meta) + ) + + // migration is "pending" if hit has a higher + // version number than currentVersion + function migrationIsPending(migration) { + return migration.version > meta.version + } + } + +} + +module.exports = Migrator diff --git a/app/scripts/migrations/002.js b/app/scripts/migrations/002.js index 0b654f825..97f427d3a 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 (meta) { + meta.version = version try { - if (data.config.provider.type === 'etherscan') { - data.config.provider.type = 'rpc' - data.config.provider.rpcTarget = 'https://rpc.metamask.io/' + if (meta.data.config.provider.type === 'etherscan') { + meta.data.config.provider.type = 'rpc' + meta.data.config.provider.rpcTarget = 'https://rpc.metamask.io/' } } catch (e) {} - return data + return Promise.resolve(meta) }, } diff --git a/app/scripts/migrations/003.js b/app/scripts/migrations/003.js index 617c55c09..b25e26e01 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 (meta) { + meta.version = version try { - if (data.config.provider.rpcTarget === oldTestRpc) { - data.config.provider.rpcTarget = newTestRpc + if (meta.data.config.provider.rpcTarget === oldTestRpc) { + meta.data.config.provider.rpcTarget = newTestRpc } } catch (e) {} - return data + return Promise.resolve(meta) }, } diff --git a/app/scripts/migrations/004.js b/app/scripts/migrations/004.js index 1329a1eed..e72eef2b7 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 (meta) { + meta.version = version try { - if (data.config.provider.type !== 'rpc') return data - switch (data.config.provider.rpcTarget) { + if (meta.data.config.provider.type !== 'rpc') return Promise.resolve(meta) + switch (meta.data.config.provider.rpcTarget) { case 'https://testrpc.metamask.io/': - data.config.provider = { + meta.data.config.provider = { type: 'testnet', } break case 'https://rpc.metamask.io/': - data.config.provider = { + meta.data.config.provider = { type: 'mainnet', } break } } catch (_) {} - return data + return Promise.resolve(meta) }, } diff --git a/package.json b/package.json index 0d0835a86..954f5a10e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "async": "^1.5.2", + "async-q": "^0.3.1", "bip39": "^2.2.0", "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", diff --git a/test/unit/migrations-test.js b/test/unit/migrations-test.js index 9ea8d5c5a..715a5feb0 100644 --- a/test/unit/migrations-test.js +++ b/test/unit/migrations-test.js @@ -1,34 +1,34 @@ -var assert = require('assert') -var path = require('path') +const assert = require('assert') +const path = require('path') -var wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) +const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) -var migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) -var migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) -var migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004')) +const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) +const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) +const migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004')) -describe('wallet1 is migrated successfully', function() { +const oldTestRpc = 'https://rawtestrpc.metamask.io/' +const newTestRpc = 'https://testrpc.metamask.io/' - it('should convert providers', function(done) { +describe('wallet1 is migrated successfully', function() { + it('should convert providers', function() { wallet1.data.config.provider = { type: 'etherscan', rpcTarget: null } - var firstResult = migration2.migrate(wallet1.data) - assert.equal(firstResult.config.provider.type, 'rpc', 'provider should be rpc') - assert.equal(firstResult.config.provider.rpcTarget, 'https://rpc.metamask.io/', 'main provider should be our rpc') - - var oldTestRpc = 'https://rawtestrpc.metamask.io/' - var newTestRpc = 'https://testrpc.metamask.io/' - firstResult.config.provider.rpcTarget = oldTestRpc - - var secondResult = migration3.migrate(firstResult) - assert.equal(secondResult.config.provider.rpcTarget, newTestRpc) - - var thirdResult = migration4.migrate(secondResult) - assert.equal(secondResult.config.provider.rpcTarget, null) - assert.equal(secondResult.config.provider.type, 'testnet') - - done() + return migration2.migrate(wallet1) + .then((firstResult) => { + assert.equal(firstResult.data.config.provider.type, 'rpc', 'provider should be rpc') + assert.equal(firstResult.data.config.provider.rpcTarget, 'https://rpc.metamask.io/', 'main provider should be our rpc') + firstResult.data.config.provider.rpcTarget = oldTestRpc + return migration3.migrate(firstResult) + }).then((secondResult) => { + assert.equal(secondResult.data.config.provider.rpcTarget, newTestRpc) + return migration4.migrate(secondResult) + }).then((thirdResult) => { + assert.equal(thirdResult.data.config.provider.rpcTarget, null) + assert.equal(thirdResult.data.config.provider.type, 'testnet') + }) + }) }) |