diff --git a/.eslintrc b/.eslintrc index d7cb8022e..7fab8f127 100644 --- a/.eslintrc +++ b/.eslintrc @@ -98,7 +98,7 @@ "no-obj-calls": 2, "no-octal": 2, "no-octal-escape": 2, - "no-path-concat": 2, + "no-path-concat": 1, "no-proto": 2, "no-redeclare": 2, "no-regex-spaces": 2, diff --git a/.nvmrc b/.nvmrc index 9773998bc..bb83eeea2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v6.0.0 +v6.3.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d9aee6ac1..5900b2b7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Current Master +- Fix bug where web3 would sometimes not be injected in time for the application. +- Fixed bug where sometimes when opening the plugin, it would not fully open until closing and re-opening. +- Got most functionality working within Firefox. + +## 2.7.0 2016-07-21 + - Added a Warning screen about storing ETH - Add buy Button! - MetaMask now throws descriptive errors when apps try to use synchronous web3 methods. diff --git a/README.md b/README.md index 34fac137c..aeb6490d2 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,12 @@ - Install [Node.js](https://nodejs.org/en/) version 6 or later. - Install local dependencies with `npm install`. - - Install gulp globally with `npm install -g gulp`. + - Install gulp globally with `npm install -g gulp-cli`. - Build the project to the `./dist/` folder with `gulp build`. - Optionally, to rebuild on file changes, run `gulp dev`. + - To package .zip files for distribution, run `gulp zip`, or run the full build & zip with `gulp dist`. + + Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built. ## Architecture @@ -18,19 +21,6 @@ npm install ``` -### Developing with Gulp - -We're using an experimental version of `gulp-cli`, so if you have the old version of gulp, you'll need to uninstall it, `npm uninstall -g gulp`, and install this one instead: - -```bash -npm install gulpjs/gulp-cli#4.0 -g -``` - -After that, you can just: -```bash -gulp dev -``` - #### In Chrome Open `Settings` > `Extensions`. @@ -39,12 +29,26 @@ Check "Developer mode". At the top, click `Load Unpacked Extension`. -Navigate to your `metamask-plugin/dist` folder. +Navigate to your `metamask-plugin/dist/chrome` folder. Click `Select`. You now have the plugin, and can click 'inspect views: background plugin' to view its dev console. +#### In Firefox + +Go to the url `about:debugging`. + +Click the button `Load Temporary Add-On`. + +Select the file `dist/firefox/manifest.json`. + +You can optionally enable debugging, and click `Debug`, for a console window that logs all of Metamask's processes to a single console. + +If you have problems debugging, try connecting to the IRC channel `#webextensions` on `irc.mozilla.org`. + +For longer questions, use the StackOverfow tag `firefox-addons`. + ### Developing on UI Only You can run `npm run ui`, and your browser should open a live-reloading demo version of the plugin UI. @@ -85,7 +89,7 @@ You can run the linter by itself with `gulp lint`. 0. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`. 1. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2). - 2. Zip the `dist` folder in this repository. - 3. Upload that zip file as the updated package. + 2. Run `gulp dist` (or `gulp zip` if you've already built) + 3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package. [1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A diff --git a/app/manifest.json b/app/manifest.json index 2c5629f7a..9f49d7b52 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,13 +1,18 @@ { - "name": "__MSG_appName__", + "name": "MetaMask", "short_name": "Metamask", - "version": "2.6.2", + "version": "2.7.0", "manifest_version": 2, - "description": "__MSG_appDescription__", + "description": "Ethereum Browser Extension", "icons": { "16": "images/icon-16.png", "128": "images/icon-128.png" }, + "applications": { + "gecko": { + "id": "webextension@metamask.io" + } + }, "default_locale": "en", "background": { "scripts": [ diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 0ffe93e3c..1eb04059d 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -3,19 +3,37 @@ const PortStream = require('./lib/port-stream.js') const ObjectMultiplex = require('./lib/obj-multiplex') const extension = require('./lib/extension') +const fs = require('fs') +const path = require('path') +const inpageText = fs.readFileSync(path.join(__dirname + '/inpage.js')).toString() + +// Eventually this streaming injection could be replaced with: +// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction +// +// But for now that is only Firefox +// If we create a FireFox-only code path using that API, +// MetaMask will be much faster loading and performant on Firefox. + if (shouldInjectWeb3()) { setupInjection() setupStreams() } function setupInjection(){ - // inject in-page script - var scriptTag = document.createElement('script') - scriptTag.src = extension.extension.getURL('scripts/inpage.js') - scriptTag.onload = function () { this.parentNode.removeChild(this) } - var container = document.head || document.documentElement - // append as first child - container.insertBefore(scriptTag, container.children[0]) + try { + + // inject in-page script + var scriptTag = document.createElement('script') + scriptTag.src = extension.extension.getURL('scripts/inpage.js') + scriptTag.textContent = inpageText + scriptTag.onload = function () { this.parentNode.removeChild(this) } + var container = document.head || document.documentElement + // append as first child + container.insertBefore(scriptTag, container.children[0]) + + } catch (e) { + console.error('Metamask injection failed.', e) + } } function setupStreams(){ @@ -44,7 +62,6 @@ function setupStreams(){ pluginStream.on('close', function () { reloadStream.write({ method: 'reset' }) }) - } function shouldInjectWeb3(){ diff --git a/app/scripts/lib/extension-instance.js b/app/scripts/lib/extension-instance.js index eeab6c6d0..eb3b8a1e9 100644 --- a/app/scripts/lib/extension-instance.js +++ b/app/scripts/lib/extension-instance.js @@ -24,14 +24,27 @@ const apis = [ function Extension () { const _this = this - let global = window - - if (window.chrome) { - global = window.chrome - } apis.forEach(function (api) { - _this[api] = global[api] + + _this[api] = null + + try { + if (chrome[api]) { + _this[api] = chrome[api] + } + } catch (e) {} + + try { + if (window[api]) { + _this[api] = window[api] + } + } catch (e) {} + + try { + _this.api = browser.extension[api] + } catch (e) {} + }) } diff --git a/app/scripts/lib/remote-store.js b/app/scripts/lib/remote-store.js index fbfab7bad..c81d5151c 100644 --- a/app/scripts/lib/remote-store.js +++ b/app/scripts/lib/remote-store.js @@ -52,7 +52,7 @@ HostStore.prototype.set = function (key, value) { HostStore.prototype.createStream = function () { var dnode = Dnode({ - // update: this._didUpdate.bind(this), + update: this._didUpdate.bind(this), }) dnode.on('remote', this._didConnect.bind(this)) return dnode diff --git a/gulpfile.js b/gulpfile.js index 941155ff4..c71ccb652 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,19 +6,23 @@ var buffer = require('vinyl-buffer') var gutil = require('gulp-util') var watch = require('gulp-watch') var sourcemaps = require('gulp-sourcemaps') +var jsoneditor = require('gulp-json-editor') +var zip = require('gulp-zip') var assign = require('lodash.assign') var livereload = require('gulp-livereload') +var brfs = require('gulp-brfs') var del = require('del') var eslint = require('gulp-eslint') var fs = require('fs') var path = require('path') +var manifest = require('./app/manifest.json') // browser reload gulp.task('dev:reload', function() { livereload.listen({ port: 35729, - // basePath: './dist/' + // basePath: './dist/firefox/' }) }) @@ -27,27 +31,40 @@ gulp.task('dev:reload', function() { gulp.task('copy:locales', copyTask({ source: './app/_locales/', - destination: './dist/_locales', + destination: './dist/firefox/_locales', })) gulp.task('copy:images', copyTask({ source: './app/images/', - destination: './dist/images', + destination: './dist/firefox/images', })) gulp.task('copy:fonts', copyTask({ source: './app/fonts/', - destination: './dist/fonts', + destination: './dist/firefox/fonts', })) gulp.task('copy:reload', copyTask({ source: './app/scripts/', - destination: './dist/scripts', + destination: './dist/firefox/scripts', pattern: '/chromereload.js', })) gulp.task('copy:root', copyTask({ source: './app/', - destination: './dist', + destination: './dist/firefox', pattern: '/*', })) -gulp.task('copy', gulp.parallel('copy:locales','copy:images','copy:fonts','copy:reload','copy:root')) +gulp.task('manifest:cleanup', function() { + return gulp.src('./dist/firefox/manifest.json') + .pipe(jsoneditor(function(json) { + delete json.applications + return json + })) + .pipe(gulp.dest('./dist/chrome', { overwrite: true })) +}) +gulp.task('copy:chrome', gulp.series( +copyTask({ + source: './dist/firefox', + destination: './dist/chrome', +}), 'manifest:cleanup')) +gulp.task('copy', gulp.series(gulp.parallel('copy:locales','copy:images','copy:fonts','copy:reload','copy:root'), 'copy:chrome')) gulp.task('copy:watch', function(){ gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy')) }) @@ -55,8 +72,8 @@ gulp.task('copy:watch', function(){ // lint js gulp.task('lint', function () { - // Ignoring node_modules, dist, and docs folders: - return gulp.src(['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js']) + // Ignoring node_modules, dist/firefox, and docs folders: + return gulp.src(['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js']) .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc')))) // eslint.format() outputs the lint results to the console. // Alternatively use eslint.formatEach() (see Docs). @@ -90,14 +107,27 @@ gulp.task('build:js', gulp.parallel('build:js:inpage','build:js:contentscript', gulp.task('clean', function clean() { - return del(['./dist']) + return del(['./dist/*']) }) +// zip tasks for distribution +gulp.task('zip:chrome', () => { + return gulp.src('dist/chrome/**') + .pipe(zip(`metamask-chrome-${manifest.version}.zip`)) + .pipe(gulp.dest('builds')); +}); +gulp.task('zip:firefox', () => { + return gulp.src('dist/firefox/**') + .pipe(zip(`metamask-firefox-${manifest.version}.zip`)) + .pipe(gulp.dest('builds')); +}); +gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox')) // high level tasks gulp.task('dev', gulp.series('dev:js', 'copy', gulp.parallel('copy:watch', 'dev:reload'))) gulp.task('build', gulp.series('clean', gulp.parallel('build:js', 'copy'))) +gulp.task('dist', gulp.series('build', 'zip')) // task generators @@ -144,13 +174,14 @@ function bundleTask(opts) { // log errors if they happen .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source(opts.filename)) + .pipe(brfs()) // optional, remove if you don't need to buffer file contents .pipe(buffer()) // optional, remove if you dont want sourcemaps .pipe(sourcemaps.init({loadMaps: true})) // loads map from browserify file // Add transformation tasks to the pipeline here. .pipe(sourcemaps.write('./')) // writes .map file - .pipe(gulp.dest('./dist/scripts')) + .pipe(gulp.dest('./dist/firefox/scripts')) .pipe(livereload()) ) diff --git a/package.json b/package.json index ef570fb94..ded561301 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "private": true, "scripts": { "start": "gulp dev", - "test": "mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\" && npm run ci", + "test": "npm run fastTest && npm run ci", + "fastTest": "mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"", "watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"", "ui": "node development/genStates.js && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", @@ -86,10 +87,13 @@ "deep-freeze-strict": "^1.1.1", "del": "^2.2.0", "gulp": "github:gulpjs/gulp#4.0", + "gulp-brfs": "^0.1.0", + "gulp-json-editor": "^2.2.1", "gulp-livereload": "^3.8.1", "gulp-sourcemaps": "^1.6.0", "gulp-util": "^3.0.7", "gulp-watch": "^4.3.5", + "gulp-zip": "^3.2.0", "jsdom": "^8.1.0", "jsdom-global": "^1.7.0", "jshint-stylish": "~0.1.5", diff --git a/test/unit/extension-test.js b/test/unit/extension-test.js index 0a03a3b97..6b695e835 100644 --- a/test/unit/extension-test.js +++ b/test/unit/extension-test.js @@ -1,6 +1,8 @@ var assert = require('assert') var sinon = require('sinon') const ethUtil = require('ethereumjs-util') +GLOBAL.chrome = {} +GLOBAL.browser = {} var path = require('path') var Extension = require(path.join(__dirname, '..', '..', 'app', 'scripts', 'lib', 'extension-instance.js')) @@ -11,7 +13,7 @@ describe('extension', function() { let extension beforeEach(function() { - window.chrome = { + GLOBAL.chrome = { alarms: 'foo' } extension = new Extension() @@ -24,13 +26,20 @@ describe('extension', function() { describe('without chrome global', function() { let extension + let realWindow beforeEach(function() { - window.chrome = undefined - window.alarms = 'foo' + realWindow = window + window = GLOBAL + GLOBAL.chrome = undefined + GLOBAL.alarms = 'foo' extension = new Extension() }) + after(function() { + window = realWindow + }) + it('should use the global apis', function() { assert.equal(extension.alarms, 'foo') }) diff --git a/ui/app/eth-store-warning.js b/ui/app/eth-store-warning.js index 4cc5da81c..d77cc0870 100644 --- a/ui/app/eth-store-warning.js +++ b/ui/app/eth-store-warning.js @@ -56,7 +56,7 @@ EthStoreWarning.prototype.render = function () { }, [ h('input', { type: 'checkbox', - onChange: this.toggleShowWarning.bind(this, event), + onChange: this.toggleShowWarning.bind(this), }), h('.warning', { style: { @@ -80,7 +80,7 @@ EthStoreWarning.prototype.render = function () { ) } -EthStoreWarning.prototype.toggleShowWarning = function (event) { +EthStoreWarning.prototype.toggleShowWarning = function () { this.props.dispatch(actions.agreeToEthWarning()) }