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 Dnode = require('dnode')
|
||||
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 LocalStorageStore = require('./lib/observable/local-storage')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
@ -16,101 +17,143 @@ const STORAGE_KEY = 'metamask-config'
|
||||
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||
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 persistence
|
||||
|
||||
let dataStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
|
||||
// initial state for first time users
|
||||
if (!dataStore.get()) {
|
||||
dataStore.put({ meta: { version: 0 }, 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)
|
||||
function loadStateFromPersistence() {
|
||||
// migrations
|
||||
let migrator = new Migrator({ migrations })
|
||||
let initialState = {
|
||||
meta: { version: migrator.defaultVersion },
|
||||
data: firstTimeState,
|
||||
}
|
||||
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) {
|
||||
// setup multiplexing
|
||||
var mx = setupMultiplex(connectionStream)
|
||||
// connect features
|
||||
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
|
||||
controller.setupPublicConfig(mx.createStream('publicConfig'))
|
||||
}
|
||||
function setupController (initState) {
|
||||
|
||||
function setupTrustedCommunication (connectionStream, originDomain) {
|
||||
// setup multiplexing
|
||||
var mx = setupMultiplex(connectionStream)
|
||||
// connect features
|
||||
setupControllerConnection(mx.createStream('controller'))
|
||||
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
|
||||
}
|
||||
//
|
||||
// MetaMask Controller
|
||||
//
|
||||
|
||||
//
|
||||
// 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
|
||||
})
|
||||
const controller = new MetamaskController({
|
||||
// User confirmation callbacks:
|
||||
showUnconfirmedMessage: triggerUi,
|
||||
unlockAccountMessage: triggerUi,
|
||||
showUnapprovedTx: triggerUi,
|
||||
// initial state
|
||||
initState,
|
||||
})
|
||||
|
||||
// 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
|
||||
@ -124,18 +167,3 @@ extension.runtime.onInstalled.addListener(function (details) {
|
||||
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 = {
|
||||
version: 2,
|
||||
const version = 2
|
||||
|
||||
migrate: function (data) {
|
||||
module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (meta) {
|
||||
meta.version = version
|
||||
try {
|
||||
if (data.config.provider.type === 'etherscan') {
|
||||
data.config.provider.type = 'rpc'
|
||||
data.config.provider.rpcTarget = 'https://rpc.metamask.io/'
|
||||
if (meta.data.config.provider.type === 'etherscan') {
|
||||
meta.data.config.provider.type = 'rpc'
|
||||
meta.data.config.provider.rpcTarget = 'https://rpc.metamask.io/'
|
||||
}
|
||||
} catch (e) {}
|
||||
return data
|
||||
return Promise.resolve(meta)
|
||||
},
|
||||
}
|
||||
|
@ -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 (meta) {
|
||||
meta.version = version
|
||||
try {
|
||||
if (data.config.provider.rpcTarget === oldTestRpc) {
|
||||
data.config.provider.rpcTarget = newTestRpc
|
||||
if (meta.data.config.provider.rpcTarget === oldTestRpc) {
|
||||
meta.data.config.provider.rpcTarget = newTestRpc
|
||||
}
|
||||
} catch (e) {}
|
||||
return data
|
||||
return Promise.resolve(meta)
|
||||
},
|
||||
}
|
||||
|
@ -1,22 +1,25 @@
|
||||
module.exports = {
|
||||
version: 4,
|
||||
const version = 4
|
||||
|
||||
migrate: function (data) {
|
||||
module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (meta) {
|
||||
meta.version = version
|
||||
try {
|
||||
if (data.config.provider.type !== 'rpc') return data
|
||||
switch (data.config.provider.rpcTarget) {
|
||||
if (meta.data.config.provider.type !== 'rpc') return Promise.resolve(meta)
|
||||
switch (meta.data.config.provider.rpcTarget) {
|
||||
case 'https://testrpc.metamask.io/':
|
||||
data.config.provider = {
|
||||
meta.data.config.provider = {
|
||||
type: 'testnet',
|
||||
}
|
||||
break
|
||||
case 'https://rpc.metamask.io/':
|
||||
data.config.provider = {
|
||||
meta.data.config.provider = {
|
||||
type: 'mainnet',
|
||||
}
|
||||
break
|
||||
}
|
||||
} catch (_) {}
|
||||
return data
|
||||
return Promise.resolve(meta)
|
||||
},
|
||||
}
|
||||
|
@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^1.5.2",
|
||||
"async-q": "^0.3.1",
|
||||
"bip39": "^2.2.0",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"browserify-derequire": "^0.9.4",
|
||||
|
@ -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'))
|
||||
|
||||
const oldTestRpc = 'https://rawtestrpc.metamask.io/'
|
||||
const newTestRpc = 'https://testrpc.metamask.io/'
|
||||
|
||||
describe('wallet1 is migrated successfully', function() {
|
||||
|
||||
it('should convert providers', function(done) {
|
||||
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')
|
||||
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')
|
||||
})
|
||||
|
||||
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')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user