Merge pull request #999 from MetaMask/obs-store2
background - introduce ObservableStore (mark II)feature/default_network_editable
commit
d30612a216
@ -0,0 +1,11 @@ |
|||||||
|
//
|
||||||
|
// The default state of MetaMask
|
||||||
|
//
|
||||||
|
|
||||||
|
module.exports = { |
||||||
|
config: { |
||||||
|
provider: { |
||||||
|
type: 'testnet', |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
@ -1,5 +0,0 @@ |
|||||||
module.exports = [ |
|
||||||
require('../migrations/002'), |
|
||||||
require('../migrations/003'), |
|
||||||
require('../migrations/004'), |
|
||||||
] |
|
@ -0,0 +1,40 @@ |
|||||||
|
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) => migration.migrate(versionedData)) |
||||||
|
.then(() => versionedData) |
||||||
|
) |
||||||
|
|
||||||
|
// migration is "pending" if hit has a higher
|
||||||
|
// version number than currentVersion
|
||||||
|
function migrationIsPending(migration) { |
||||||
|
return migration.version > versionedData.meta.version |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
generateInitialState (initState) { |
||||||
|
return { |
||||||
|
meta: { |
||||||
|
version: this.defaultVersion, |
||||||
|
}, |
||||||
|
data: initState, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = Migrator |
@ -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) |
|
||||||
} |
|
@ -1,13 +1,16 @@ |
|||||||
|
const version = 2 |
||||||
|
|
||||||
module.exports = { |
module.exports = { |
||||||
version: 2, |
version, |
||||||
|
|
||||||
migrate: function (data) { |
migrate: function (versionedData) { |
||||||
|
versionedData.meta.version = version |
||||||
try { |
try { |
||||||
if (data.config.provider.type === 'etherscan') { |
if (versionedData.data.config.provider.type === 'etherscan') { |
||||||
data.config.provider.type = 'rpc' |
versionedData.data.config.provider.type = 'rpc' |
||||||
data.config.provider.rpcTarget = 'https://rpc.metamask.io/' |
versionedData.data.config.provider.rpcTarget = 'https://rpc.metamask.io/' |
||||||
} |
} |
||||||
} catch (e) {} |
} catch (e) {} |
||||||
return data |
return Promise.resolve(versionedData) |
||||||
}, |
}, |
||||||
} |
} |
||||||
|
@ -1,15 +1,17 @@ |
|||||||
var oldTestRpc = 'https://rawtestrpc.metamask.io/' |
const version = 3 |
||||||
var newTestRpc = 'https://testrpc.metamask.io/' |
const oldTestRpc = 'https://rawtestrpc.metamask.io/' |
||||||
|
const newTestRpc = 'https://testrpc.metamask.io/' |
||||||
|
|
||||||
module.exports = { |
module.exports = { |
||||||
version: 3, |
version, |
||||||
|
|
||||||
migrate: function (data) { |
migrate: function (versionedData) { |
||||||
|
versionedData.meta.version = version |
||||||
try { |
try { |
||||||
if (data.config.provider.rpcTarget === oldTestRpc) { |
if (versionedData.data.config.provider.rpcTarget === oldTestRpc) { |
||||||
data.config.provider.rpcTarget = newTestRpc |
versionedData.data.config.provider.rpcTarget = newTestRpc |
||||||
} |
} |
||||||
} catch (e) {} |
} catch (e) {} |
||||||
return data |
return Promise.resolve(versionedData) |
||||||
}, |
}, |
||||||
} |
} |
||||||
|
@ -1,22 +1,25 @@ |
|||||||
|
const version = 4 |
||||||
|
|
||||||
module.exports = { |
module.exports = { |
||||||
version: 4, |
version,
|
||||||
|
|
||||||
migrate: function (data) { |
migrate: function (versionedData) { |
||||||
|
versionedData.meta.version = version |
||||||
try { |
try { |
||||||
if (data.config.provider.type !== 'rpc') return data |
if (versionedData.data.config.provider.type !== 'rpc') return Promise.resolve(versionedData) |
||||||
switch (data.config.provider.rpcTarget) { |
switch (versionedData.data.config.provider.rpcTarget) { |
||||||
case 'https://testrpc.metamask.io/': |
case 'https://testrpc.metamask.io/': |
||||||
data.config.provider = { |
versionedData.data.config.provider = { |
||||||
type: 'testnet', |
type: 'testnet', |
||||||
} |
} |
||||||
break |
break |
||||||
case 'https://rpc.metamask.io/': |
case 'https://rpc.metamask.io/': |
||||||
data.config.provider = { |
versionedData.data.config.provider = { |
||||||
type: 'mainnet', |
type: 'mainnet', |
||||||
} |
} |
||||||
break |
break |
||||||
} |
} |
||||||
} catch (_) {} |
} catch (_) {} |
||||||
return data |
return Promise.resolve(versionedData) |
||||||
}, |
}, |
||||||
} |
} |
||||||
|
@ -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) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}, |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
/* 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'), |
||||||
|
] |
@ -1,58 +1,10 @@ |
|||||||
var ConfigManager = require('../../app/scripts/lib/config-manager') |
const ObservableStore = require('obs-store') |
||||||
|
const clone = require('clone') |
||||||
|
const ConfigManager = require('../../app/scripts/lib/config-manager') |
||||||
|
const firstTimeState = require('../../app/scripts/first-time-state') |
||||||
const STORAGE_KEY = 'metamask-config' |
const STORAGE_KEY = 'metamask-config' |
||||||
const extend = require('xtend') |
|
||||||
|
|
||||||
module.exports = function() { |
module.exports = function() { |
||||||
return new ConfigManager({ loadData, setData }) |
let store = new ObservableStore(clone(firstTimeState)) |
||||||
} |
return new ConfigManager({ store }) |
||||||
|
|
||||||
function loadData () { |
|
||||||
var oldData = getOldStyleData() |
|
||||||
var newData |
|
||||||
|
|
||||||
try { |
|
||||||
newData = JSON.parse(window.localStorage[STORAGE_KEY]) |
|
||||||
} catch (e) {} |
|
||||||
|
|
||||||
var data = extend({ |
|
||||||
meta: { |
|
||||||
version: 0, |
|
||||||
}, |
|
||||||
data: { |
|
||||||
config: { |
|
||||||
provider: { |
|
||||||
type: 'testnet', |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, oldData || null, newData || null) |
|
||||||
return data |
|
||||||
} |
|
||||||
|
|
||||||
function getOldStyleData () { |
|
||||||
var config, wallet, seedWords |
|
||||||
|
|
||||||
var result = { |
|
||||||
meta: { version: 0 }, |
|
||||||
data: {}, |
|
||||||
} |
|
||||||
|
|
||||||
try { |
|
||||||
config = JSON.parse(window.localStorage['config']) |
|
||||||
result.data.config = config |
|
||||||
} catch (e) {} |
|
||||||
try { |
|
||||||
wallet = JSON.parse(window.localStorage['lightwallet']) |
|
||||||
result.data.wallet = wallet |
|
||||||
} catch (e) {} |
|
||||||
try { |
|
||||||
seedWords = window.localStorage['seedWords'] |
|
||||||
result.data.seedWords = seedWords |
|
||||||
} catch (e) {} |
|
||||||
|
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
function setData (data) { |
|
||||||
window.localStorage[STORAGE_KEY] = JSON.stringify(data) |
|
||||||
} |
} |
@ -1,34 +1,34 @@ |
|||||||
var assert = require('assert') |
const assert = require('assert') |
||||||
var path = require('path') |
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')) |
const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) |
||||||
var migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) |
const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) |
||||||
var migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004')) |
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 } |
wallet1.data.config.provider = { type: 'etherscan', rpcTarget: null } |
||||||
|
|
||||||
var firstResult = migration2.migrate(wallet1.data) |
return migration2.migrate(wallet1) |
||||||
assert.equal(firstResult.config.provider.type, 'rpc', 'provider should be rpc') |
.then((firstResult) => { |
||||||
assert.equal(firstResult.config.provider.rpcTarget, 'https://rpc.metamask.io/', 'main provider should be our rpc') |
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') |
||||||
var oldTestRpc = 'https://rawtestrpc.metamask.io/' |
firstResult.data.config.provider.rpcTarget = oldTestRpc |
||||||
var newTestRpc = 'https://testrpc.metamask.io/' |
return migration3.migrate(firstResult) |
||||||
firstResult.config.provider.rpcTarget = oldTestRpc |
}).then((secondResult) => { |
||||||
|
assert.equal(secondResult.data.config.provider.rpcTarget, newTestRpc) |
||||||
var secondResult = migration3.migrate(firstResult) |
return migration4.migrate(secondResult) |
||||||
assert.equal(secondResult.config.provider.rpcTarget, newTestRpc) |
}).then((thirdResult) => { |
||||||
|
assert.equal(thirdResult.data.config.provider.rpcTarget, null) |
||||||
var thirdResult = migration4.migrate(secondResult) |
assert.equal(thirdResult.data.config.provider.type, 'testnet') |
||||||
assert.equal(secondResult.config.provider.rpcTarget, null) |
}) |
||||||
assert.equal(secondResult.config.provider.type, 'testnet') |
|
||||||
|
|
||||||
done() |
|
||||||
}) |
}) |
||||||
}) |
}) |
||||||
|
|
||||||
|
Loading…
Reference in new issue