diff options
Merge pull request #81 from MetaMask/MigratableConfig
Migratable config
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | app/manifest.json | 2 | ||||
-rw-r--r-- | app/scripts/background.js | 22 | ||||
-rw-r--r-- | app/scripts/lib/config-manager-singleton.js | 3 | ||||
-rw-r--r-- | app/scripts/lib/config-manager.js | 157 | ||||
-rw-r--r-- | app/scripts/lib/idStore.js | 48 | ||||
-rw-r--r-- | package.json | 6 | ||||
-rw-r--r-- | test/helper.js | 6 | ||||
-rw-r--r-- | test/unit/config-manager-test.js | 71 |
9 files changed, 270 insertions, 48 deletions
diff --git a/.gitignore b/.gitignore index 0ef0ceb2e..476b197db 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ app/bower_components test/bower_components package -.DS_Store
\ No newline at end of file +.DS_Store +builds/ diff --git a/app/manifest.json b/app/manifest.json index 6faedf6d4..915295914 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,6 +1,6 @@ { "name": "__MSG_appName__", - "version": "1.2.0", + "version": "1.2.1", "manifest_version": 2, "description": "__MSG_appDescription__", "icons": { diff --git a/app/scripts/background.js b/app/scripts/background.js index 1a0d36ef8..772c1de89 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -9,8 +9,7 @@ const PortStream = require('./lib/port-stream.js') const MetaMaskProvider = require('web3-provider-engine/zero.js') const IdentityStore = require('./lib/idStore') const createTxNotification = require('./lib/tx-notification.js') - -console.log('ready to roll') +const configManager = require('./lib/config-manager-singleton') // // connect to other contexts @@ -37,10 +36,9 @@ function handleEthRpcRequestStream(stream){ // state and network // -var config = getConfig() var idStore = new IdentityStore() var zeroClient = MetaMaskProvider({ - rpcUrl: config.rpcTarget, + rpcUrl: configManager.getCurrentRpcAddress(), getAccounts: function(cb){ var selectedAddress = idStore.getSelectedAddress() var result = selectedAddress ? [selectedAddress] : [] @@ -62,7 +60,7 @@ function getState(){ var state = extend( ethStore.getState(), idStore.getState(), - getConfig() + configManager.getConfig() ) return state } @@ -177,22 +175,10 @@ function addUnconfirmedTx(txParams, cb){ // called from popup function setRpcTarget(rpcTarget){ - var config = getConfig() - config.rpcTarget = rpcTarget - setConfig(config) + configManager.setRpcTarget(rpcTarget) chrome.runtime.reload() } -function getConfig(){ - return extend({ - rpcTarget: 'https://rawtestrpc.metamask.io/', - }, JSON.parse(localStorage['config'] || '{}')) -} - -function setConfig(state){ - localStorage['config'] = JSON.stringify(state) -} - // util function jsonParseStream(){ diff --git a/app/scripts/lib/config-manager-singleton.js b/app/scripts/lib/config-manager-singleton.js new file mode 100644 index 000000000..5915c401b --- /dev/null +++ b/app/scripts/lib/config-manager-singleton.js @@ -0,0 +1,3 @@ +var ConfigManager = require('./config-manager') + +module.exports = new ConfigManager() diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js new file mode 100644 index 000000000..682b34637 --- /dev/null +++ b/app/scripts/lib/config-manager.js @@ -0,0 +1,157 @@ +const Migrator = require('pojo-migrator') +const extend = require('xtend') + +const STORAGE_KEY = 'metamask-config' +var DEFAULT_RPC = 'https://rawtestrpc.metamask.io/' + +/* The config-manager is a convenience object + * wrapping a pojo-migrator. + * + * It exists mostly to allow the creation of + * convenience methods to access and persist + * particular portions of the state. + */ +module.exports = ConfigManager +function ConfigManager() { + + /* The migrator exported on the config-manager + * 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. + */ + this.migrator = new Migrator({ + + // 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. + migrations: [], + + // How to load initial config. + // Includes step on migrating pre-pojo-migrator data. + loadData: loadData, + + // How to persist migrated config. + setData: function(data) { + window.localStorage[STORAGE_KEY] = JSON.stringify(data) + }, + }) +} + +ConfigManager.prototype.setConfig = function(config) { + var data = this.migrator.getData() + data.config = config + this.setData(data) +} + +ConfigManager.prototype.setRpcTarget = function(rpcUrl) { + var config = this.getConfig() + config.provider = { + type: 'rpc', + rpcTarget: rpcUrl, + } + this.setConfig(config) +} + +ConfigManager.prototype.getConfig = function() { + var data = this.migrator.getData() + if ('config' in data) { + return data.config + } else { + return { + provider: { + type: 'rpc', + rpcTarget: DEFAULT_RPC, + } + } + } +} + +ConfigManager.prototype.setData = function(data) { + this.migrator.saveData(data) +} + +ConfigManager.prototype.getData = function() { + return this.migrator.getData() +} + +ConfigManager.prototype.setWallet = function(wallet) { + var data = this.migrator.getData() + data.wallet = wallet + this.setData(data) +} + +ConfigManager.prototype.getWallet = function() { + return this.migrator.getData().wallet +} + +// Takes a boolean +ConfigManager.prototype.setShowSeedWords = function(should) { + var data = this.migrator.getData() + data.showSeedWords = should + this.setData(data) +} + +ConfigManager.prototype.getShouldShowSeedWords = function() { + var data = this.migrator.getData() + return data.showSeedWords +} + +ConfigManager.prototype.getCurrentRpcAddress = function() { + var config = this.getConfig() + if (!config) return null + return config.provider && config.provider.rpcTarget ? config.provider.rpcTarget : DEFAULT_RPC +} + +ConfigManager.prototype.clearWallet = function() { + var data = this.getConfig() + delete data.wallet + this.setData(data) +} + +function loadData() { + + var oldData = getOldStyleData() + var newData + try { + newData = JSON.parse(window.localStorage[STORAGE_KEY]) + } catch (e) {} + + var data = extend({ + version: 0, + data: { + config: { + rpcTarget: DEFAULT_RPC, + } + } + }, oldData ? oldData : null, newData ? 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 +} + diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index ea873e627..1bc1ebcb2 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -8,6 +8,7 @@ const clone = require('clone') const extend = require('xtend') const createId = require('web3-provider-engine/util/random-id') const autoFaucet = require('./auto-faucet') +const configManager = require('./config-manager-singleton') module.exports = IdentityStore @@ -41,14 +42,16 @@ function IdentityStore(ethStore) { IdentityStore.prototype.createNewVault = function(password, entropy, cb){ delete this._keyStore - delete window.localStorage['lightwallet'] + configManager.clearWallet() this._createIdmgmt(password, null, entropy, (err) => { if (err) return cb(err) - var seedWords = this._idmgmt.getSeed() - this._cacheSeedWordsUntilConfirmed(seedWords) + this._loadIdentities() this._didUpdate() this._autoFaucet() + + configManager.setShowSeedWords(true) + var seedWords = this._idmgmt.getSeed() cb(null, seedWords) }) } @@ -68,19 +71,28 @@ IdentityStore.prototype.setStore = function(store){ } IdentityStore.prototype.clearSeedWordCache = function(cb) { - delete window.localStorage['seedWords'] + configManager.setShowSeedWords(false) cb() } IdentityStore.prototype.getState = function(){ - const cachedSeeds = window.localStorage['seedWords'] + var seedWords = this.getSeedIfUnlocked() + var wallet = configManager.getWallet() return clone(extend(this._currentState, { - isInitialized: !!window.localStorage['lightwallet'] && !cachedSeeds, + isInitialized: !!configManager.getWallet() && !seedWords, isUnlocked: this._isUnlocked(), - seedWords: cachedSeeds, + seedWords: seedWords, })) } +IdentityStore.prototype.getSeedIfUnlocked = function() { + var showSeed = configManager.getShouldShowSeedWords() + var idmgmt = this._idmgmt + var shouldShow = showSeed && !!idmgmt + var seedWords = shouldShow ? idmgmt.getSeed() : null + return seedWords +} + IdentityStore.prototype.getSelectedAddress = function(){ return this._currentState.selectedAddress } @@ -181,10 +193,6 @@ IdentityStore.prototype._isUnlocked = function(){ return result } -IdentityStore.prototype._cacheSeedWordsUntilConfirmed = function(seedWords) { - window.localStorage['seedWords'] = seedWords -} - // load identities from keyStoreet IdentityStore.prototype._loadIdentities = function(){ if (!this._isUnlocked()) throw new Error('not unlocked') @@ -216,14 +224,14 @@ IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){ var keyStore = null LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => { if (err) return cb(err) - var serializedKeystore = window.localStorage['lightwallet'] + var serializedKeystore = configManager.getWallet() if (seed) { keyStore = this._restoreFromSeed(password, seed, derivedKey) - // returning user, recovering from localStorage + // returning user, recovering from storage } else if (serializedKeystore) { - keyStore = this._loadFromLocalStorage(serializedKeystore, derivedKey, cb) + keyStore = LightwalletKeyStore.deserialize(serializedKeystore) var isCorrect = keyStore.isDerivedKeyCorrect(derivedKey) if (!isCorrect) return cb(new Error('Lightwallet - password incorrect')) @@ -249,15 +257,11 @@ IdentityStore.prototype._restoreFromSeed = function(password, seed, derivedKey) keyStore.setDefaultHdDerivationPath(this.hdPathString) keyStore.generateNewAddress(derivedKey, 3) - window.localStorage['lightwallet'] = keyStore.serialize() - console.log('restored from seed. saved to keystore localStorage') + configManager.setWallet(keyStore.serialize()) + console.log('restored from seed. saved to keystore') return keyStore } -IdentityStore.prototype._loadFromLocalStorage = function(serializedKeystore, derivedKey) { - return LightwalletKeyStore.deserialize(serializedKeystore) -} - IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) { var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy) var keyStore = new LightwalletKeyStore(secretSeed, derivedKey, this.hdPathString) @@ -265,8 +269,8 @@ IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) { keyStore.setDefaultHdDerivationPath(this.hdPathString) keyStore.generateNewAddress(derivedKey, 3) - window.localStorage['lightwallet'] = keyStore.serialize() - console.log('saved to keystore localStorage') + configManager.setWallet(keyStore.serialize()) + console.log('saved to keystore') return keyStore } diff --git a/package.json b/package.json index f482fd4f6..be61f1d14 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "private": true, "scripts": { "start": "gulp dev", - "test": "mocha --compilers js:babel-register --recursive" + "test": "mocha --require test/helper.js --compilers js:babel-register --recursive", + "watch": "mocha watch --compilers js:babel-register --recursive" }, "dependencies": { "async": "^1.5.2", @@ -21,6 +22,7 @@ "inject-css": "^0.1.1", "metamask-ui": "^1.5.0", "multiplex": "^6.7.0", + "pojo-migrator": "^2.1.0", "pumpify": "^1.3.4", "readable-stream": "^2.0.5", "through2": "^2.0.1", @@ -39,10 +41,10 @@ "gulp-util": "^3.0.7", "gulp-watch": "^4.3.5", "jsdom": "^8.1.0", + "jsdom-global": "^1.7.0", "jshint-stylish": "~0.1.5", "lodash.assign": "^4.0.6", "mocha": "^2.4.5", - "mocha-jsdom": "^1.1.0", "mocha-sinon": "^1.1.5", "sinon": "^1.17.3", "vinyl-buffer": "^1.0.0", diff --git a/test/helper.js b/test/helper.js index 7746b90e0..4c7f8b4c6 100644 --- a/test/helper.js +++ b/test/helper.js @@ -1,4 +1,2 @@ -require('mocha-sinon')() -var jsdom = require('mocha-jsdom') -jsdom() - +require('jsdom-global')() +window.localStorage = {} diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js new file mode 100644 index 000000000..10b6716d6 --- /dev/null +++ b/test/unit/config-manager-test.js @@ -0,0 +1,71 @@ +var assert = require('assert') +var ConfigManager = require('../../app/scripts/lib/config-manager') +var configManager + +describe('config-manager', function() { + + before(function() { + window.localStorage = {} // Hacking localStorage support into JSDom + configManager = new ConfigManager() + }) + + describe('#setConfig', function() { + window.localStorage = {} // Hacking localStorage support into JSDom + + it('should set the config key', function () { + var testConfig = { + provider: { + type: 'rpc', + rpcTarget: 'foobar' + } + } + configManager.setConfig(testConfig) + var result = configManager.getData() + + assert.equal(result.config.provider.type, testConfig.provider.type) + assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget) + }) + + it('setting wallet should not overwrite config', function() { + var testConfig = { + provider: { + type: 'rpc', + rpcTarget: 'foobar' + } + } + configManager.setConfig(testConfig) + + var testWallet = { + name: 'this is my fake wallet' + } + configManager.setWallet(testWallet) + + var result = configManager.getData() + assert.equal(result.wallet.name, testWallet.name, 'wallet name is set') + assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget) + + testConfig.provider.type = 'something else!' + configManager.setConfig(testConfig) + + result = configManager.getData() + assert.equal(result.wallet.name, testWallet.name, 'wallet name is set') + assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget) + assert.equal(result.config.provider.type, testConfig.provider.type) + }) + }) + + describe('rpc manipulations', function() { + it('changing rpc should return a different rpc', function() { + var firstRpc = 'first' + var secondRpc = 'second' + + configManager.setRpcTarget(firstRpc) + var firstResult = configManager.getCurrentRpcAddress() + assert.equal(firstResult, firstRpc) + + configManager.setRpcTarget(secondRpc) + var secondResult = configManager.getCurrentRpcAddress() + assert.equal(secondResult, secondRpc) + }) + }) +}) |