mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
background - introduce ObservableStore
This commit is contained in:
parent
cc5e9aca4f
commit
8012ede126
@ -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) {
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
|
50
app/scripts/lib/observable/host.js
Normal file
50
app/scripts/lib/observable/host.js
Normal file
@ -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
|
33
app/scripts/lib/observable/index.js
Normal file
33
app/scripts/lib/observable/index.js
Normal file
@ -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
|
51
app/scripts/lib/observable/remote.js
Normal file
51
app/scripts/lib/observable/remote.js
Normal file
@ -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
|
13
app/scripts/lib/observable/util/transform.js
Normal file
13
app/scripts/lib/observable/util/transform.js
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
var initPublicState = this.store.get()
|
||||
var publicConfigStore = new HostStore(initPublicState, { readOnly: true })
|
||||
|
||||
// subscribe to changes
|
||||
this.configManager.subscribe(function (state) {
|
||||
storeSetFromObj(publicConfigStore, configToPublic(state))
|
||||
})
|
||||
// sync publicConfigStore with transform
|
||||
transformStore(this.store, publicConfigStore, selectPublicState)
|
||||
|
||||
this.keyringController.on('newAccount', (account) => {
|
||||
autoFaucet(account)
|
||||
})
|
||||
|
||||
// config substate
|
||||
function configToPublic (state) {
|
||||
return {
|
||||
selectedAccount: state.selectedAccount,
|
||||
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
|
||||
|
@ -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 = {}
|
||||
|
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user