diff options
background - introduce ObservableStore
-rw-r--r-- | app/scripts/background.js | 45 | ||||
-rw-r--r-- | app/scripts/lib/config-manager.js | 10 | ||||
-rw-r--r-- | app/scripts/lib/inpage-provider.js | 18 | ||||
-rw-r--r-- | app/scripts/lib/observable/host.js | 50 | ||||
-rw-r--r-- | app/scripts/lib/observable/index.js | 33 | ||||
-rw-r--r-- | app/scripts/lib/observable/remote.js | 51 | ||||
-rw-r--r-- | app/scripts/lib/observable/util/transform.js | 13 | ||||
-rw-r--r-- | app/scripts/lib/remote-store.js | 97 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 53 | ||||
-rw-r--r-- | mock-dev.js | 8 | ||||
-rw-r--r-- | test/integration/lib/idStore-migrator-test.js | 33 |
11 files changed, 224 insertions, 187 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index 6b7926526..f3c0b52b3 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -13,15 +13,18 @@ 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, + // initial state + initState: loadData(), }) +// setup state persistence +controller.store.subscribe(setData) + const txManager = controller.txManager function triggerUi () { if (!popupIsOpen) notification.show() @@ -112,13 +115,7 @@ function updateBadge () { // data :: setters/getters function loadData () { - var oldData = getOldStyleData() - var newData - try { - newData = JSON.parse(window.localStorage[STORAGE_KEY]) - } catch (e) {} - - var data = extend({ + let defaultData = { meta: { version: 0, }, @@ -129,32 +126,16 @@ function loadData () { }, }, }, - }, oldData || null, newData || null) - return data -} - -function getOldStyleData () { - var config, wallet, seedWords - - var result = { - meta: { version: 0 }, - data: {}, } + var persisted 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) {} + persisted = JSON.parse(window.localStorage[STORAGE_KEY]) + } catch (err) { + persisted = null + } - return result + return extend(defaultData, persisted) } function setData (data) { diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 3a1f12ac0..01e6ccc3c 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -19,6 +19,7 @@ module.exports = ConfigManager function ConfigManager (opts) { // ConfigManager is observable and will emit updates this._subs = [] + this.store = opts.store /* The migrator exported on the config-manager * has two methods the user should be concerned with: @@ -36,12 +37,9 @@ function ConfigManager (opts) { // 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, + // Data persistence methods + loadData: () => this.store.get(), + setData: (value) => this.store.put(value), }) } diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 11bd5cc3a..64301be78 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,7 +1,7 @@ const Streams = require('mississippi') const StreamProvider = require('web3-stream-provider') const ObjectMultiplex = require('./obj-multiplex') -const RemoteStore = require('./remote-store.js').RemoteStore +const RemoteStore = require('./observable/remote') const createRandomId = require('./random-id') module.exports = MetamaskInpageProvider @@ -72,13 +72,13 @@ MetamaskInpageProvider.prototype.send = function (payload) { case 'eth_accounts': // read from localStorage - selectedAccount = self.publicConfigStore.get('selectedAccount') + selectedAccount = self.publicConfigStore.get().selectedAccount result = selectedAccount ? [selectedAccount] : [] break case 'eth_coinbase': // read from localStorage - selectedAccount = self.publicConfigStore.get('selectedAccount') + selectedAccount = self.publicConfigStore.get().selectedAccount result = selectedAccount || '0x0000000000000000000000000000000000000000' break @@ -117,9 +117,15 @@ MetamaskInpageProvider.prototype.isMetaMask = true function remoteStoreWithLocalStorageCache (storageKey) { // read local cache - var initState = JSON.parse(localStorage[storageKey] || '{}') - var store = new RemoteStore(initState) - // cache the latest state locally + let initState + try { + initState = JSON.parse(localStorage[storageKey] || '{}') + } catch (err) { + initState = {} + } + // intialize store + const store = new RemoteStore(initState) + // write local cache store.subscribe(function (state) { localStorage[storageKey] = JSON.stringify(state) }) diff --git a/app/scripts/lib/observable/host.js b/app/scripts/lib/observable/host.js new file mode 100644 index 000000000..69f674be8 --- /dev/null +++ b/app/scripts/lib/observable/host.js @@ -0,0 +1,50 @@ +const Dnode = require('dnode') +const ObservableStore = require('./index') +const endOfStream = require('end-of-stream') + +// +// HostStore +// +// plays host to many RemoteStores and sends its state over a stream +// + +class HostStore extends ObservableStore { + + constructor (initState, opts) { + super(initState) + this.opts = opts || {} + } + + createStream () { + const self = this + // setup remotely exposed api + let remoteApi = {} + if (!self.opts.readOnly) { + remoteApi.put = (newState) => self.put(newState) + } + // listen for connection to remote + const dnode = Dnode(remoteApi) + dnode.on('remote', (remote) => { + // setup update subscription lifecycle + const updateHandler = (state) => remote.put(state) + self._onConnect(updateHandler) + endOfStream(dnode, () => self._onDisconnect(updateHandler)) + }) + return dnode + } + + _onConnect (updateHandler) { + // subscribe to updates + this.subscribe(updateHandler) + // send state immediately + updateHandler(this.get()) + } + + _onDisconnect (updateHandler) { + // unsubscribe to updates + this.unsubscribe(updateHandler) + } + +} + +module.exports = HostStore diff --git a/app/scripts/lib/observable/index.js b/app/scripts/lib/observable/index.js new file mode 100644 index 000000000..d193e5554 --- /dev/null +++ b/app/scripts/lib/observable/index.js @@ -0,0 +1,33 @@ +const EventEmitter = require('events').EventEmitter + +class ObservableStore extends EventEmitter { + + constructor (initialState) { + super() + this._state = initialState + } + + get () { + return this._state + } + + put (newState) { + this._put(newState) + } + + subscribe (handler) { + this.on('update', handler) + } + + unsubscribe (handler) { + this.removeListener('update', handler) + } + + _put (newState) { + this._state = newState + this.emit('update', newState) + } + +} + +module.exports = ObservableStore diff --git a/app/scripts/lib/observable/remote.js b/app/scripts/lib/observable/remote.js new file mode 100644 index 000000000..b5a3254a2 --- /dev/null +++ b/app/scripts/lib/observable/remote.js @@ -0,0 +1,51 @@ +const Dnode = require('dnode') +const ObservableStore = require('./index') +const endOfStream = require('end-of-stream') + +// +// RemoteStore +// +// connects to a HostStore and receives its latest state +// + +class RemoteStore extends ObservableStore { + + constructor (initState, opts) { + super(initState) + this.opts = opts || {} + this._remote = null + } + + put (newState) { + if (!this._remote) throw new Error('RemoteStore - "put" called before connection to HostStore') + this._put(newState) + this._remote.put(newState) + } + + createStream () { + const self = this + const dnode = Dnode({ + put: (newState) => self._put(newState), + }) + // listen for connection to remote + dnode.once('remote', (remote) => { + // setup connection lifecycle + self._onConnect(remote) + endOfStream(dnode, () => self._onDisconnect()) + }) + return dnode + } + + _onConnect (remote) { + this._remote = remote + this.emit('connected') + } + + _onDisconnect () { + this._remote = null + this.emit('disconnected') + } + +} + +module.exports = RemoteStore
\ No newline at end of file diff --git a/app/scripts/lib/observable/util/transform.js b/app/scripts/lib/observable/util/transform.js new file mode 100644 index 000000000..87946f402 --- /dev/null +++ b/app/scripts/lib/observable/util/transform.js @@ -0,0 +1,13 @@ + +module.exports = transformStore + + +function transformStore(inStore, outStore, stateTransform) { + const initState = stateTransform(inStore.get()) + outStore.put(initState) + inStore.subscribe((inState) => { + const outState = stateTransform(inState) + outStore.put(outState) + }) + return outStore +}
\ No newline at end of file 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/metamask-controller.js b/app/scripts/metamask-controller.js index 1fc97e81d..8e0eaf54c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -6,26 +6,38 @@ const KeyringController = require('./keyring-controller') const NoticeController = require('./notice-controller') 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 ObservableStore = require('./lib/observable/') +const HostStore = require('./lib/observable/host') +const transformStore = require('./lib/observable/util/transform') 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) + this.state = { network: 'loading' } + + // observable state store + this.store = new ObservableStore(opts.initState) + // config manager + this.configManager = new ConfigManager({ + store: this.store, + }) + // key mgmt this.keyringController = new KeyringController({ configManager: this.configManager, getNetwork: this.getStateNetwork.bind(this), }) + this.keyringController.on('newAccount', (account) => { + autoFaucet(account) + }) // notices this.noticeController = new NoticeController({ configManager: this.configManager, @@ -228,29 +240,20 @@ module.exports = class MetamaskController extends EventEmitter { 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)) - }) - - this.keyringController.on('newAccount', (account) => { - autoFaucet(account) - }) - - // config substate - function configToPublic (state) { - return { - selectedAccount: state.selectedAccount, + var initPublicState = this.store.get() + var publicConfigStore = new HostStore(initPublicState, { readOnly: true }) + + // sync publicConfigStore with transform + transformStore(this.store, publicConfigStore, selectPublicState) + + function selectPublicState(state) { + let result = { selectedAccount: undefined } + try { + result.selectedAccount = state.data.config.selectedAccount + } catch (err) { + console.warn('Error in "selectPublicState": ' + err.message) } - } - // dump obj into store - function storeSetFromObj (store, obj) { - Object.keys(obj).forEach(function (key) { - store.set(key, obj[key]) - }) + return result } return publicConfigStore diff --git a/mock-dev.js b/mock-dev.js index 283bc2c79..dfd0b4961 100644 --- a/mock-dev.js +++ b/mock-dev.js @@ -47,11 +47,13 @@ const controller = new MetamaskController({ showUnconfirmedMessage: noop, unlockAccountMessage: noop, showUnapprovedTx: noop, - // Persistence Methods: - setData, - loadData, + // initial state + initState: loadData(), }) +// setup state persistence +controller.store.subscribe(setData) + // Stub out localStorage for non-browser environments if (!window.localStorage) { window.localStorage = {} diff --git a/test/integration/lib/idStore-migrator-test.js b/test/integration/lib/idStore-migrator-test.js index 4ae30411d..1ceaac442 100644 --- a/test/integration/lib/idStore-migrator-test.js +++ b/test/integration/lib/idStore-migrator-test.js @@ -1,25 +1,23 @@ -var ConfigManager = require('../../../app/scripts/lib/config-manager') -var IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator') -var SimpleKeyring = require('../../../app/scripts/keyrings/simple') -var normalize = require('../../../app/scripts/lib/sig-util').normalize +const ObservableStore = require('../../../app/scripts/lib/observable/') +const ConfigManager = require('../../../app/scripts/lib/config-manager') +const IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator') +const SimpleKeyring = require('../../../app/scripts/keyrings/simple') +const normalize = require('../../../app/scripts/lib/sig-util').normalize -var oldStyleVault = require('../mocks/oldVault.json') -var badStyleVault = require('../mocks/badVault.json') +const oldStyleVault = require('../mocks/oldVault.json') +const badStyleVault = require('../mocks/badVault.json') -var STORAGE_KEY = 'metamask-config' -var PASSWORD = '12345678' -var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase() -var SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner' - -var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9' +const PASSWORD = '12345678' +const FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase() +const BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9' +const SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner' QUnit.module('Old Style Vaults', { beforeEach: function () { - window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault) + let store = new ObservableStore(oldStyleVault) this.configManager = new ConfigManager({ - loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, - setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, + store: store, }) this.migrator = new IdStoreMigrator({ @@ -46,11 +44,10 @@ QUnit.test('migrator:migratedVaultForPassword', function (assert) { QUnit.module('Old Style Vaults with bad HD seed', { beforeEach: function () { - window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault) + let store = new ObservableStore(badStyleVault) this.configManager = new ConfigManager({ - loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) }, - setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) }, + store: store, }) this.migrator = new IdStoreMigrator({ |