diff --git a/.gitignore b/.gitignore index 5f2d2f551..2bf38cad4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ npm-debug.log node_modules +yarn.lock app/bower_components test/bower_components diff --git a/CHANGELOG.md b/CHANGELOG.md index 25eb9b915..e7d4a09fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,17 @@ # Changelog ## Current Master + +- Add ability for internationalization. +- Will now throw an error if the `to` field in txParams is not valid. +- Will strip null values from the `to` field. - Fix flashing to Log in screen after logging in or restoring from seed phrase. - Increase tap areas for menu buttons on mobile - Change all fonts in new-ui onboarding to Roboto, size 400 - Add a welcome screen to new-ui onboarding flow - Make new-ui create password screen responsive - Hide network dropdown before account is initialized -- Add ability for internationalization. +- Fix bug that could prevent MetaMask from saving the latest vault. ## 4.2.0 Tue Mar 06 2018 diff --git a/app/manifest.json b/app/manifest.json index 6c5ed686b..6fcf6cd7c 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -56,6 +56,7 @@ ], "permissions": [ "storage", + "unlimitedStorage", "clipboardWrite", "http://localhost:8545/", "https://*.infura.io/" diff --git a/app/scripts/background.js b/app/scripts/background.js index 601ae0372..ef5513ec7 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,9 +1,11 @@ const urlUtil = require('url') const endOfStream = require('end-of-stream') const pump = require('pump') +const debounce = require('debounce-stream') const log = require('loglevel') const extension = require('extensionizer') const LocalStorageStore = require('obs-store/lib/localStorage') +const LocalStore = require('./lib/local-store') const storeTransform = require('obs-store/lib/transform') const asStream = require('obs-store/lib/asStream') const ExtensionPlatform = require('./platforms/extension') @@ -44,6 +46,8 @@ let openMetamaskTabsIDs = {} // state persistence const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) +const localStore = new LocalStore() +let versionedData // initialization flow initialize().catch(log.error) @@ -64,12 +68,23 @@ async function initialize () { async function loadStateFromPersistence () { // migrations const migrator = new Migrator({ migrations }) + // read from disk - let versionedData = diskStore.getState() || migrator.generateInitialState(firstTimeState) + // first from preferred, async API: + versionedData = (await localStore.get()) || + diskStore.getState() || + migrator.generateInitialState(firstTimeState) + // migrate data versionedData = await migrator.migrateData(versionedData) + if (!versionedData) { + throw new Error('MetaMask - migrator returned undefined') + } + // write to disk + if (localStore.isSupported) localStore.set(versionedData) diskStore.putState(versionedData) + // return just the data return versionedData.data } @@ -102,16 +117,30 @@ function setupController (initState) { // setup state persistence pump( asStream(controller.store), + debounce(1000), storeTransform(versionifyData), - asStream(diskStore) + storeTransform(syncDataWithExtension), + asStream(diskStore), + (error) => { + log.error('pump hit error', error) + } ) function versionifyData (state) { - const versionedData = diskStore.getState() versionedData.data = state return versionedData } + function syncDataWithExtension(state) { + if (localStore.isSupported) { + localStore.set(state) + .catch((err) => { + log.error('error setting state in local store:', err) + }) + } + return state + } + // // connect to other contexts // diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js new file mode 100644 index 000000000..1cf00dd30 --- /dev/null +++ b/app/scripts/lib/local-store.js @@ -0,0 +1,38 @@ +// We should not rely on local storage in an extension! +// We should use this instead! +// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/local + +const extension = require('extensionizer') +const { promisify } = require('util') + +module.exports = class ExtensionStore { + constructor() { + this.isSupported = !!(extension.storage.local) + if (!this.isSupported) { + log.error('Storage local API not available.') + } + const local = extension.storage.local + this._get = promisify(local.get).bind(local) + this._set = promisify(local.set).bind(local) + } + + async get() { + if (!this.isSupported) return undefined + const result = await this._get() + // extension.storage.local always returns an obj + // if the object is empty, treat it as undefined + if (isEmpty(result)) { + return undefined + } else { + return result + } + } + + async set(state) { + return this._set(state) + } +} + +function isEmpty(obj) { + return Object.keys(obj).length === 0 +} diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/lib/tx-gas-utils.js index 6f6ff7852..0fa9dd8d4 100644 --- a/app/scripts/lib/tx-gas-utils.js +++ b/app/scripts/lib/tx-gas-utils.js @@ -4,7 +4,7 @@ const { BnMultiplyByFraction, bnToHex, } = require('./util') -const addHexPrefix = require('ethereumjs-util').addHexPrefix +const { addHexPrefix, isValidAddress } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. /* @@ -113,12 +113,14 @@ module.exports = class TxGasUtil { } } validateRecipient (txParams) { - if (txParams.to === '0x') { + if (txParams.to === '0x' || txParams.to === null ) { if (txParams.data) { delete txParams.to } else { throw new Error('Invalid recipient address') } + } else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) { + throw new Error('Invalid recipient address') } return txParams } diff --git a/old-ui/app/keychains/hd/restore-vault.js b/old-ui/app/keychains/hd/restore-vault.js index 222172dfd..d334d8e5f 100644 --- a/old-ui/app/keychains/hd/restore-vault.js +++ b/old-ui/app/keychains/hd/restore-vault.js @@ -140,6 +140,19 @@ RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { // check seed var seedBox = document.querySelector('textarea.twelve-word-phrase') var seed = seedBox.value.trim() + + // true if the string has more than a space between words. + if (seed.split(' ').length > 1) { + this.warning = 'there can only be a space between words' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // true if seed contains a character that is not between a-z or a space + if (!seed.match(/^[a-z ]+$/)) { + this.warning = 'seed words only have lowercase characters' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } if (seed.split(' ').length !== 12) { this.warning = 'seed phrases are 12 words long' this.props.dispatch(actions.displayWarning(this.warning)) diff --git a/package.json b/package.json index a80b7e28e..00587ece6 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "clone": "^2.1.1", "copy-to-clipboard": "^3.0.8", "debounce": "^1.0.0", + "debounce-stream": "^2.0.0", "deep-extend": "^0.5.0", "detect-node": "^2.0.3", "disc": "^1.3.2", diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/app/keychains/hd/restore-vault.js index d1761f17d..685094854 100644 --- a/ui/app/keychains/hd/restore-vault.js +++ b/ui/app/keychains/hd/restore-vault.js @@ -144,6 +144,19 @@ RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { // check seed var seedBox = document.querySelector('textarea.twelve-word-phrase') var seed = seedBox.value.trim() + + // true if the string has more than a space between words. + if (seed.split(' ').length > 1) { + this.warning = 'there can only a space between words' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // true if seed contains a character that is not between a-z or a space + if (!seed.match(/^[a-z ]+$/)) { + this.warning = 'seed words only have lowercase characters' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } if (seed.split(' ').length !== 12) { this.warning = 'seed phrases are 12 words long' this.props.dispatch(actions.displayWarning(this.warning))