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 = { |
||||
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) |
||||
}, |
||||
} |
||||
|
@ -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) |
||||
}, |
||||
} |
||||
|
@ -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) |
||||
}, |
||||
} |
||||
|
@ -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 extend = require('xtend') |
||||
|
||||
module.exports = function() { |
||||
return new ConfigManager({ loadData, setData }) |
||||
} |
||||
|
||||
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) |
||||
let store = new ObservableStore(clone(firstTimeState)) |
||||
return new ConfigManager({ store }) |
||||
} |
@ -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') |
||||
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') |
||||
}) |
||||
|
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
|
Loading…
Reference in new issue