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:
parent
3bc996878b
commit
b33c51c0a6
@ -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' })
|
|
||||||
}
|
|
31
app/scripts/lib/migrator/index.js
Normal file
31
app/scripts/lib/migrator/index.js
Normal 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
|
@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user