1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

migrations - introduce promise-based migrator

This commit is contained in:
kumavis 2017-01-12 01:17:05 -08:00
parent 3bc996878b
commit b33c51c0a6
7 changed files with 212 additions and 144 deletions

View File

@ -1,7 +1,8 @@
const urlUtil = require('url') const urlUtil = require('url')
const Dnode = require('dnode') const Dnode = require('dnode')
const eos = require('end-of-stream') 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 migrations = require('./lib/migrations')
const LocalStorageStore = require('./lib/observable/local-storage') const LocalStorageStore = require('./lib/observable/local-storage')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
@ -16,101 +17,143 @@ const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
let popupIsOpen = false 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 and Persistence
// //
// state persistence function loadStateFromPersistence() {
// migrations
let dataStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) let migrator = new Migrator({ migrations })
// initial state for first time users let initialState = {
if (!dataStore.get()) { meta: { version: migrator.defaultVersion },
dataStore.put({ meta: { version: 0 }, data: firstTimeState }) data: firstTimeState,
}
// migrations
let migrator = new Migrator({
migrations,
// Data persistence methods
loadData: () => dataStore.get(),
setData: (newState) => dataStore.put(newState),
})
//
// MetaMask Controller
//
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))
//
// 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)
} }
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),
])
} }
function setupUntrustedCommunication (connectionStream, originDomain) { function setupController (initState) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
function setupTrustedCommunication (connectionStream, originDomain) { //
// setup multiplexing // MetaMask Controller
var mx = setupMultiplex(connectionStream) //
// connect features
setupControllerConnection(mx.createStream('controller'))
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
}
// const controller = new MetamaskController({
// remote features // User confirmation callbacks:
// showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
function setupControllerConnection (stream) { showUnapprovedTx: triggerUi,
controller.stream = stream // initial state
var api = controller.getApi() initState,
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
})
}) })
// 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)
}
}
function setupUntrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
function setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
setupControllerConnection(mx.createStream('controller'))
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
}
//
// 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
})
})
}
//
// 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' })
}
return Promise.resolve()
} }
// //
// User Interface setup // Etc...
// //
// popup trigger // popup trigger
@ -123,19 +166,4 @@ extension.runtime.onInstalled.addListener(function (details) {
if ((details.reason === 'install') && (!METAMASK_DEBUG)) { if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) 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' })
}

View File

@ -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

View File

@ -1,13 +1,16 @@
module.exports = { const version = 2
version: 2,
migrate: function (data) { module.exports = {
version,
migrate: function (meta) {
meta.version = version
try { try {
if (data.config.provider.type === 'etherscan') { if (meta.data.config.provider.type === 'etherscan') {
data.config.provider.type = 'rpc' meta.data.config.provider.type = 'rpc'
data.config.provider.rpcTarget = 'https://rpc.metamask.io/' meta.data.config.provider.rpcTarget = 'https://rpc.metamask.io/'
} }
} catch (e) {} } catch (e) {}
return data return Promise.resolve(meta)
}, },
} }

View File

@ -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 (meta) {
meta.version = version
try { try {
if (data.config.provider.rpcTarget === oldTestRpc) { if (meta.data.config.provider.rpcTarget === oldTestRpc) {
data.config.provider.rpcTarget = newTestRpc meta.data.config.provider.rpcTarget = newTestRpc
} }
} catch (e) {} } catch (e) {}
return data return Promise.resolve(meta)
}, },
} }

View File

@ -1,22 +1,25 @@
module.exports = { const version = 4
version: 4,
migrate: function (data) { module.exports = {
version,
migrate: function (meta) {
meta.version = version
try { try {
if (data.config.provider.type !== 'rpc') return data if (meta.data.config.provider.type !== 'rpc') return Promise.resolve(meta)
switch (data.config.provider.rpcTarget) { switch (meta.data.config.provider.rpcTarget) {
case 'https://testrpc.metamask.io/': case 'https://testrpc.metamask.io/':
data.config.provider = { meta.data.config.provider = {
type: 'testnet', type: 'testnet',
} }
break break
case 'https://rpc.metamask.io/': case 'https://rpc.metamask.io/':
data.config.provider = { meta.data.config.provider = {
type: 'mainnet', type: 'mainnet',
} }
break break
} }
} catch (_) {} } catch (_) {}
return data return Promise.resolve(meta)
}, },
} }

View File

@ -37,6 +37,7 @@
}, },
"dependencies": { "dependencies": {
"async": "^1.5.2", "async": "^1.5.2",
"async-q": "^0.3.1",
"bip39": "^2.2.0", "bip39": "^2.2.0",
"browser-passworder": "^2.0.3", "browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4", "browserify-derequire": "^0.9.4",

View File

@ -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'))
const oldTestRpc = 'https://rawtestrpc.metamask.io/'
const newTestRpc = 'https://testrpc.metamask.io/'
describe('wallet1 is migrated successfully', function() { describe('wallet1 is migrated successfully', function() {
it('should convert providers', function() {
it('should convert providers', function(done) {
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()
}) })
}) })