Merge branch 'master' into AddTokenList

feature/default_network_editable
Dan Finlay 8 years ago
commit a741cc4fc4
  1. 4
      .eslintignore
  2. 7
      .eslintrc
  3. 25
      .gitignore
  4. 95
      CHANGELOG.md
  5. 2
      Dockerfile
  6. 134
      README.md
  7. 1
      app/currencies.json
  8. 4
      app/manifest.json
  9. 2
      app/scripts/account-import-strategies/index.js
  10. 50
      app/scripts/background.js
  11. 9
      app/scripts/config.js
  12. 3
      app/scripts/contentscript.js
  13. 8
      app/scripts/controllers/address-book.js
  14. 10
      app/scripts/controllers/currency.js
  15. 129
      app/scripts/controllers/network.js
  16. 8
      app/scripts/controllers/preferences.js
  17. 98
      app/scripts/controllers/transactions.js
  18. 12
      app/scripts/first-time-state.js
  19. 21
      app/scripts/inpage.js
  20. 4
      app/scripts/keyring-controller.js
  21. 47
      app/scripts/lib/auto-reload.js
  22. 8
      app/scripts/lib/buy-eth-url.js
  23. 78
      app/scripts/lib/config-manager.js
  24. 4
      app/scripts/lib/eth-store.js
  25. 5
      app/scripts/lib/inpage-provider.js
  26. 4
      app/scripts/lib/message-manager.js
  27. 43
      app/scripts/lib/migrator/index.js
  28. 5
      app/scripts/lib/notification-manager.js
  29. 4
      app/scripts/lib/personal-message-manager.js
  30. 30
      app/scripts/lib/tx-utils.js
  31. 233
      app/scripts/metamask-controller.js
  32. 2
      app/scripts/migrations/002.js
  33. 2
      app/scripts/migrations/003.js
  34. 2
      app/scripts/migrations/004.js
  35. 2
      app/scripts/migrations/005.js
  36. 2
      app/scripts/migrations/006.js
  37. 2
      app/scripts/migrations/007.js
  38. 2
      app/scripts/migrations/008.js
  39. 2
      app/scripts/migrations/009.js
  40. 2
      app/scripts/migrations/010.js
  41. 2
      app/scripts/migrations/011.js
  42. 2
      app/scripts/migrations/012.js
  43. 34
      app/scripts/migrations/013.js
  44. 34
      app/scripts/migrations/014.js
  45. 11
      app/scripts/migrations/_multi-keyring.js
  46. 2
      app/scripts/migrations/index.js
  47. 6
      app/scripts/popup-core.js
  48. 2
      app/scripts/popup.js
  49. 2
      circle.yml
  50. 2
      development/announcer.js
  51. 14
      docs/add-to-chrome.md
  52. 14
      docs/add-to-firef.md
  53. 25
      docs/adding-new-networks.md
  54. 10
      docs/developing-on-deps.md
  55. 35
      docs/development-visualization.md
  56. 15
      docs/notices.md
  57. 19
      docs/publishing.md
  58. 6
      docs/ui-dev-mode.md
  59. 8
      docs/ui-mock-mode.md
  60. 14
      gulpfile.js
  61. 31
      mascara/README.md
  62. 14
      mascara/src/background.js
  63. 88
      mascara/src/lib/index-db-controller.js
  64. 5
      mascara/src/mascara.js
  65. 1
      mascara/src/proxy.js
  66. 16
      mascara/src/ui.js
  67. 7
      mascara/test/helpers.js
  68. 21
      mascara/test/index.html
  69. 22
      mascara/test/index.js
  70. 4
      mascara/test/jquery-3.1.0.min.js
  71. 119
      mascara/test/lib/first-time.js
  72. 13
      mascara/test/testem.yml
  73. 40
      mascara/test/util/mascara-test-helper.js
  74. 5
      mascara/test/window-load.js
  75. 3
      mascara/ui/index.html
  76. 1
      notices/archive/notice_1.md
  77. 8
      notices/archive/notice_2.md
  78. 2
      notices/notice-nonce.json
  79. 2
      notices/notices.json
  80. 35
      package.json
  81. 8
      test/helper.js
  82. 4
      test/integration/helpers.js
  83. 8
      test/integration/index.js
  84. 2
      test/integration/lib/first-time.js
  85. 15
      test/lib/migrations/001.json
  86. 7
      test/lib/mock-config-manager.js
  87. 12
      test/lib/mock-encryptor.js
  88. 14
      test/lib/mock-simple-keychain.js
  89. 18
      test/lib/mock-store.js
  90. 8
      test/unit/account-link-test.js
  91. 23
      test/unit/actions/config_test.js
  92. 21
      test/unit/actions/save_account_label_test.js
  93. 23
      test/unit/actions/set_selected_account_test.js
  94. 92
      test/unit/actions/tx_test.js
  95. 15
      test/unit/actions/view_info_test.js
  96. 13
      test/unit/actions/warning_test.js
  97. 16
      test/unit/address-book-controller.js
  98. 10
      test/unit/components/binary-renderer-test.js
  99. 51
      test/unit/components/bn-as-decimal-input-test.js
  100. 77
      test/unit/components/pending-tx-test.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1 +1,5 @@
app/scripts/lib/extension-instance.js app/scripts/lib/extension-instance.js
test/integration/bundle.js
test/integration/jquery-3.1.0.min.js
test/integration/helpers.js
test/integration/lib/first-time.js

@ -1,7 +1,7 @@
{ {
"parserOptions": { "parserOptions": {
"sourceType": "module", "sourceType": "module",
"ecmaVersion": 6, "ecmaVersion": 2017,
"ecmaFeatures": { "ecmaFeatures": {
"experimentalObjectRestSpread": true, "experimentalObjectRestSpread": true,
"impliedStrict": true, "impliedStrict": true,
@ -17,10 +17,13 @@
"env": { "env": {
"es6": true, "es6": true,
"node": true, "node": true,
"browser": true "browser": true,
"mocha" : true
}, },
"plugins": [ "plugins": [
"mocha",
"chai"
], ],
"globals": { "globals": {

25
.gitignore vendored

@ -1,18 +1,27 @@
dist
npm-debug.log npm-debug.log
node_modules node_modules
temp package-lock.json
.tmp
.sass-cache
app/bower_components app/bower_components
test/bower_components test/bower_components
package package
temp
.tmp
.sass-cache
.DS_Store .DS_Store
app/.DS_Store
dist
builds/ builds/
disc/ disc/
notes.txt
app/.DS_Store
development/bundle.js
builds.zip builds.zip
test/integration/bundle.js
development/bundle.js
development/states.js development/states.js
test/integration/bundle.js
test/background.js
test/bundle.js
test/test-bundle.js
notes.txt

@ -2,8 +2,103 @@
## Current Master ## Current Master
## 3.7.8 2017-6-12
- Add a `ethereum:` prefix to the QR code address
- The default network on installation is now MainNet
- Fix currency API URL from cryptonator.
- Update gasLimit params with every new block seen.
## 3.7.7 2017-6-8
- Fix bug where metamask would show old data after computer being asleep or disconnected from the internet.
## 3.7.6 2017-6-5
- Fix bug that prevented publishing contracts.
## 3.7.5 2017-6-5
- Prevent users from sending to the `0x0` address.
- Provide useful errors when entering bad characters in ENS name.
- Add ability to copy addresses from transaction confirmation view.
## 3.7.4 2017-6-2
- Fix bug with inflight cache that caused some block lookups to return bad values (affected OasisDex).
- Fixed bug with gas limit calculation that would sometimes create unsubmittable gas limits.
## 3.7.3 2017-6-1
- Rebuilt to fix cache clearing bug.
## 3.7.2 2017-5-31
- Now when switching networks sites that use web3 will reload
- Now when switching networks the extension does not restart
- Cleanup decimal bugs in our gas inputs.
- Fix bug where submit button was enabled for invalid gas inputs.
- Now enforce 95% of block's gasLimit to protect users.
- Removing provider-engine from the inpage provider. This fixes some error handling inconsistencies introduced in 3.7.0.
- Added "inflight cache", which prevents identical requests from clogging up the network, dramatically improving ENS performance.
- Fixed bug where filter subscriptions would sometimes fail to unsubscribe.
- Some contracts will now display logos instead of jazzicons.
- Some contracts will now have names displayed in the confirmation view.
## 3.7.0 2017-5-23
- Add Transaction Number (nonce) to transaction list.
- Label the pending tx icon with a tooltip.
- Fix bug where website filters would pile up and not deallocate when leaving a site.
- Continually resubmit pending txs for a period of time to ensure successful broadcast.
- ENS names will no longer resolve to their owner if no resolver is set. Resolvers must be explicitly set and configured.
## 3.6.5 2017-5-17
- Fix bug where edited gas parameters would not take effect.
- Trim currency list.
- Enable decimals in our gas prices.
- Fix reset button.
- Fix event filter bug introduced by newer versions of Geth.
- Fix bug where decimals in gas inputs could result in strange values.
## 3.6.4 2017-5-8
- Fix main-net ENS resolution.
## 3.6.3 2017-5-8
- Fix bug that could stop newer versions of Geth from working with MetaMask.
## 3.6.2 2017-5-8
- Input gas price in Gwei.
- Enforce Safe Gas Minimum recommended by EthGasStation.
- Fix bug where block-tracker could stop polling for new blocks.
- Reduce UI size by removing internal web3.
- Fix bug where gas parameters would not properly update on adjustment.
## 3.6.1 2017-4-30
- Made fox less nosy.
- Fix bug where error was reported in debugger console when Chrome opened a new window.
## 3.6.0 2017-4-26
- Add Rinkeby Test Network to our network list.
## 3.5.4 2017-4-25
- Fix occasional nonce tracking issue.
- Fix bug where some events would not be emitted by web3.
- Fix bug where an error would be thrown when composing signatures for networks with large ID values.
## 3.5.3 2017-4-24
- Popup new transactions in Firefox. - Popup new transactions in Firefox.
- Fix transition issue from account detail screen. - Fix transition issue from account detail screen.
- Revise buy screen for more modularity.
- Fixed some other small bugs.
## 3.5.2 2017-3-28 ## 3.5.2 2017-3-28

@ -1,4 +1,4 @@
FROM node:6 FROM node:7
MAINTAINER kumavis MAINTAINER kumavis
# setup app dir # setup app dir

@ -18,11 +18,15 @@ If you're a web dapp developer, we've got two types of guides for you:
Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built. Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built.
## Installing Local Builds on Chrome ### Running Tests
Requires `mocha` installed. Run `npm install -g mocha`.
To install your locally built extension on Chrome, [follow this guide](http://stackoverflow.com/a/24577660/272576). Then just run `npm test`.
The built extension is stored in `./dist/chrome/`. You can also test with a continuously watching process, via `npm run watch`.
You can run the linter by itself with `gulp lint`.
## Architecture ## Architecture
@ -41,126 +45,22 @@ npm start
npm run dist npm run dist
``` ```
#### In Chrome
Open `Settings` > `Extensions`.
Check "Developer mode".
At the top, click `Load Unpacked Extension`.
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.
Some actions will crash the app, so this is only for tuning aesthetics, but it allows live-reloading styles, which is a much faster feedback loop than reloading the full extension.
### Developing on UI with Mocked Background Process
You can run `npm run mock` and your browser should open a live-reloading demo version of the plugin UI, just like the `npm run ui`, except that it tries to actually perform all normal operations.
It does not yet connect to a real blockchain (this could be a good test feature later, connecting to a test blockchain), so only local operations work.
You can reset the mock ui at any time with the `Reset` button at the top of the screen.
### Developing on Dependencies
To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies:
1. Clone the dependency locally.
2. `npm install` in its folder.
3. Run `npm link` in its folder.
4. Run `npm link $DEP_NAME` in this project folder.
5. Next time you `npm start` it will watch the dependency for changes as well!
### Running Tests
Requires `mocha` installed. Run `npm install -g mocha`.
Then just run `npm test`.
You can also test with a continuously watching process, via `npm run watch`.
You can run the linter by itself with `gulp lint`.
#### Writing Browser Tests #### Writing Browser Tests
To write tests that will be run in the browser using QUnit, add your test files to `test/integration/lib`. To write tests that will be run in the browser using QUnit, add your test files to `test/integration/lib`.
### Deploying the UI ## Other Docs
You must be authorized already on the MetaMask plugin.
0. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`. - [How to add custom build to Chrome](./docs/add-to-chrome.md)
1. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2). - [How to add custom build to Firefox](./docs/add-to-firefox.md)
2. Run `gulp dist` (or `gulp zip` if you've already built) - [How to develop a live-reloading UI](./docs/ui-dev-mode.md)
3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package. - [Publishing Guide](./docs/publishing.md)
- [How to develop an in-browser mocked UI](./docs/ui-mock-mode.md)
- [How to live reload on local dependency changes](./docs/developing-on-deps.md)
- [How to add new networks to the Provider Menu](./docs/adding-new-networks.md)
- [How to manage notices that appear when the app starts up](./docs/notices.md)
- [How to generate a visualization of this repository's development](./docs/development-visualization.md)
[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 [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
### Generate Development Visualization
This will generate a video of the repo commit history.
Install preqs:
```
brew install gource
brew install ffmpeg
```
From the repo dir, pipe `gource` into `ffmpeg`:
```
gource \
--seconds-per-day .1 \
--user-scale 1.5 \
--default-user-image "./images/icon-512.png" \
--viewport 1280x720 \
--auto-skip-seconds .1 \
--multi-sampling \
--stop-at-end \
--highlight-users \
--hide mouse,progress \
--file-idle-time 0 \
--max-files 0 \
--background-colour 000000 \
--font-size 18 \
--date-format "%b %d, %Y" \
--highlight-dirs \
--user-friction 0.1 \
--title "MetaMask Development History" \
--output-ppm-stream - \
--output-framerate 30 \
| ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4
```
## Generating Notices
To add a notice:
```
npm run generateNotice
```
To delete a notice:
```
npm run deleteNotice
```

File diff suppressed because one or more lines are too long

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.5.2", "version": "3.7.8",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
@ -58,7 +58,7 @@
"storage", "storage",
"clipboardWrite", "clipboardWrite",
"http://localhost:8545/", "http://localhost:8545/",
"https://www.cryptonator.com/" "https://api.cryptonator.com/"
], ],
"web_accessible_resources": [ "web_accessible_resources": [
"scripts/inpage.js" "scripts/inpage.js"

@ -4,7 +4,7 @@ const ethUtil = require('ethereumjs-util')
const accountImporter = { const accountImporter = {
importAccount(strategy, args) { importAccount (strategy, args) {
try { try {
const importer = this.strategies[strategy] const importer = this.strategies[strategy]
const privateKeyHex = importer.apply(null, args) const privateKeyHex = importer.apply(null, args)

@ -1,6 +1,5 @@
const urlUtil = require('url') const urlUtil = require('url')
const endOfStream = require('end-of-stream') const endOfStream = require('end-of-stream')
const asyncQ = require('async-q')
const pipe = require('pump') const pipe = require('pump')
const LocalStorageStore = require('obs-store/lib/localStorage') const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform') const storeTransform = require('obs-store/lib/transform')
@ -30,38 +29,32 @@ let popupIsOpen = false
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
// initialization flow // initialization flow
asyncQ.waterfall([ initialize().catch(console.error)
() => loadStateFromPersistence(),
(initState) => setupController(initState), async function initialize () {
]) const initState = await loadStateFromPersistence()
.then(() => console.log('MetaMask initialization complete.')) await setupController(initState)
.catch((err) => { console.error(err) }) console.log('MetaMask initialization complete.')
}
// //
// State and Persistence // State and Persistence
// //
function loadStateFromPersistence() { async function loadStateFromPersistence () {
// migrations // migrations
let migrator = new Migrator({ migrations }) const migrator = new Migrator({ migrations })
let initialState = migrator.generateInitialState(firstTimeState) // read from disk
return asyncQ.waterfall([ let versionedData = diskStore.getState() || migrator.generateInitialState(firstTimeState)
// read from disk // migrate data
() => Promise.resolve(diskStore.getState() || initialState), versionedData = await migrator.migrateData(versionedData)
// migrate data // write to disk
(versionedData) => migrator.migrateData(versionedData), diskStore.putState(versionedData)
// write to disk // return just the data
(versionedData) => { return versionedData.data
diskStore.putState(versionedData)
return Promise.resolve(versionedData)
},
// resolve to just data
(versionedData) => Promise.resolve(versionedData.data),
])
} }
function setupController (initState) { function setupController (initState) {
// //
// MetaMask Controller // MetaMask Controller
// //
@ -85,8 +78,8 @@ function setupController (initState) {
diskStore diskStore
) )
function versionifyData(state) { function versionifyData (state) {
let versionedData = diskStore.getState() const versionedData = diskStore.getState()
versionedData.data = state versionedData.data = state
return versionedData return versionedData
} }
@ -121,13 +114,13 @@ function setupController (initState) {
// //
updateBadge() updateBadge()
controller.txManager.on('updateBadge', updateBadge) controller.txController.on('updateBadge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge) controller.messageManager.on('updateBadge', updateBadge)
// plugin badge text // plugin badge text
function updateBadge () { function updateBadge () {
var label = '' var label = ''
var unapprovedTxCount = controller.txManager.unapprovedTxCount var unapprovedTxCount = controller.txController.unapprovedTxCount
var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
var count = unapprovedTxCount + unapprovedMsgCount var count = unapprovedTxCount + unapprovedMsgCount
if (count) { if (count) {
@ -138,7 +131,6 @@ function setupController (initState) {
} }
return Promise.resolve() return Promise.resolve()
} }
// //

@ -1,16 +1,15 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask' const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const DEFAULT_RPC_URL = TESTNET_RPC_URL const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
module.exports = { module.exports = {
network: { network: {
default: DEFAULT_RPC_URL,
mainnet: MAINET_RPC_URL, mainnet: MAINET_RPC_URL,
testnet: TESTNET_RPC_URL, ropsten: ROPSTEN_RPC_URL,
morden: TESTNET_RPC_URL,
kovan: KOVAN_RPC_URL, kovan: KOVAN_RPC_URL,
rinkeby: RINKEBY_RPC_URL,
}, },
} }

@ -61,7 +61,6 @@ function setupStreams () {
// ignore unused channels (handled by background) // ignore unused channels (handled by background)
mx.ignoreStream('provider') mx.ignoreStream('provider')
mx.ignoreStream('publicConfig') mx.ignoreStream('publicConfig')
mx.ignoreStream('reload')
} }
function shouldInjectWeb3 () { function shouldInjectWeb3 () {
@ -77,7 +76,7 @@ function doctypeCheck () {
} }
} }
function suffixCheck() { function suffixCheck () {
var prohibitedTypes = ['xml', 'pdf'] var prohibitedTypes = ['xml', 'pdf']
var currentUrl = window.location.href var currentUrl = window.location.href
var currentRegex var currentRegex

@ -39,11 +39,11 @@ class AddressBookController {
// pushed object is an object of two fields. Current behavior does not set an // pushed object is an object of two fields. Current behavior does not set an
// upper limit to the number of addresses. // upper limit to the number of addresses.
_addToAddressBook (address, name) { _addToAddressBook (address, name) {
let addressBook = this._getAddressBook() const addressBook = this._getAddressBook()
let identities = this._getIdentities() const identities = this._getIdentities()
let addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name }) const addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name })
let identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() }) const identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() })
// trigger this condition if we own this address--no need to overwrite. // trigger this condition if we own this address--no need to overwrite.
if (identitiesIndex !== -1) { if (identitiesIndex !== -1) {
return Promise.resolve(addressBook) return Promise.resolve(addressBook)

@ -45,15 +45,17 @@ class CurrencyController {
updateConversionRate () { updateConversionRate () {
const currentCurrency = this.getCurrentCurrency() const currentCurrency = this.getCurrentCurrency()
return fetch(`https://www.cryptonator.com/api/ticker/eth-${currentCurrency}`) return fetch(`https://api.cryptonator.com/api/ticker/eth-${currentCurrency}`)
.then(response => response.json()) .then(response => response.json())
.then((parsedResponse) => { .then((parsedResponse) => {
this.setConversionRate(Number(parsedResponse.ticker.price)) this.setConversionRate(Number(parsedResponse.ticker.price))
this.setConversionDate(Number(parsedResponse.timestamp)) this.setConversionDate(Number(parsedResponse.timestamp))
}).catch((err) => { }).catch((err) => {
console.warn('MetaMask - Failed to query currency conversion.') if (err) {
this.setConversionRate(0) console.warn('MetaMask - Failed to query currency conversion.')
this.setConversionDate('N/A') this.setConversionRate(0)
this.setConversionDate('N/A')
}
}) })
} }

@ -0,0 +1,129 @@
const EventEmitter = require('events')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend')
const EthQuery = require('eth-query')
const RPC_ADDRESS_LIST = require('../config.js').network
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
module.exports = class NetworkController extends EventEmitter {
constructor (config) {
super()
this.networkStore = new ObservableStore('loading')
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.providerStore = new ObservableStore(config.provider)
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
this._providerListeners = {}
this.on('networkDidChange', this.lookupNetwork)
this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget}))
}
get provider () {
return this._proxy
}
set provider (provider) {
this._provider = provider
}
initializeProvider (opts) {
this.providerInit = opts
this._provider = MetaMaskProvider(opts)
this._proxy = new Proxy(this._provider, {
get: (obj, name) => {
if (name === 'on') return this._on.bind(this)
return this._provider[name]
},
set: (obj, name, value) => {
this._provider[name] = value
},
})
this.provider.on('block', this._logBlock.bind(this))
this.provider.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this.provider)
this.lookupNetwork()
return this.provider
}
switchNetwork (providerInit) {
this.setNetworkState('loading')
const newInit = extend(this.providerInit, providerInit)
this.providerInit = newInit
this._provider.removeAllListeners()
this._provider.stop()
this.provider = MetaMaskProvider(newInit)
// apply the listners created by other controllers
Object.keys(this._providerListeners).forEach((key) => {
this._providerListeners[key].forEach((handler) => this._provider.addListener(key, handler))
})
this.emit('networkDidChange')
}
verifyNetwork () {
// Check network when restoring connectivity:
if (this.isNetworkLoading()) this.lookupNetwork()
}
getNetworkState () {
return this.networkStore.getState()
}
setNetworkState (network) {
return this.networkStore.putState(network)
}
isNetworkLoading () {
return this.getNetworkState() === 'loading'
}
lookupNetwork () {
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network)
this.setNetworkState(network)
})
}
setRpcTarget (rpcUrl) {
this.providerStore.updateState({
type: 'rpc',
rpcTarget: rpcUrl,
})
}
getCurrentRpcAddress () {
const provider = this.getProviderConfig()
if (!provider) return null
return this.getRpcAddressForType(provider.type)
}
setProviderType (type) {
if (type === this.getProviderConfig().type) return
const rpcTarget = this.getRpcAddressForType(type)
this.providerStore.updateState({type, rpcTarget})
}
getProviderConfig () {
return this.providerStore.getState()
}
getRpcAddressForType (type, provider = this.getProviderConfig()) {
if (RPC_ADDRESS_LIST[type]) return RPC_ADDRESS_LIST[type]
return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC
}
_logBlock (block) {
log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
this.verifyNetwork()
}
_on (event, handler) {
if (!this._providerListeners[event]) this._providerListeners[event] = []
this._providerListeners[event].push(handler)
this._provider.on(event, handler)
}
}

@ -36,8 +36,8 @@ class PreferencesController {
} }
addToFrequentRpcList (_url) { addToFrequentRpcList (_url) {
let rpcList = this.getFrequentRpcList() const rpcList = this.getFrequentRpcList()
let index = rpcList.findIndex((element) => { return element === _url }) const index = rpcList.findIndex((element) => { return element === _url })
if (index !== -1) { if (index !== -1) {
rpcList.splice(index, 1) rpcList.splice(index, 1)
} }
@ -53,13 +53,9 @@ class PreferencesController {
getFrequentRpcList () { getFrequentRpcList () {
return this.store.getState().frequentRpcList return this.store.getState().frequentRpcList
} }
// //
// PRIVATE METHODS // PRIVATE METHODS
// //
} }
module.exports = PreferencesController module.exports = PreferencesController

@ -4,11 +4,14 @@ const extend = require('xtend')
const Semaphore = require('semaphore') const Semaphore = require('semaphore')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthQuery = require('eth-query') const TxProviderUtil = require('../lib/tx-utils')
const TxProviderUtil = require('./lib/tx-utils') const createId = require('../lib/random-id')
const createId = require('./lib/random-id') const denodeify = require('denodeify')
module.exports = class TransactionManager extends EventEmitter { const RETRY_LIMIT = 200
const RESUBMIT_INTERVAL = 10000 // Ten seconds
module.exports = class TransactionController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.store = new ObservableStore(extend({ this.store = new ObservableStore(extend({
@ -20,17 +23,19 @@ module.exports = class TransactionManager extends EventEmitter {
this.txHistoryLimit = opts.txHistoryLimit this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider this.provider = opts.provider
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.query = new EthQuery(this.provider) this.query = opts.ethQuery
this.txProviderUtils = new TxProviderUtil(this.provider) this.txProviderUtils = new TxProviderUtil(this.query)
this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
this.signEthTx = opts.signTransaction this.signEthTx = opts.signTransaction
this.nonceLock = Semaphore(1) this.nonceLock = Semaphore(1)
// memstore is computed from a few different stores // memstore is computed from a few different stores
this._updateMemstore() this._updateMemstore()
this.store.subscribe(() => this._updateMemstore() ) this.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore() ) this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore() ) this.preferencesStore.subscribe(() => this._updateMemstore())
this.continuallyResubmitPendingTxs()
} }
getState () { getState () {
@ -38,7 +43,7 @@ module.exports = class TransactionManager extends EventEmitter {
} }
getNetwork () { getNetwork () {
return this.networkStore.getState().network return this.networkStore.getState()
} }
getSelectedAddress () { getSelectedAddress () {
@ -47,8 +52,8 @@ module.exports = class TransactionManager extends EventEmitter {
// Returns the tx list // Returns the tx list
getTxList () { getTxList () {
let network = this.getNetwork() const network = this.getNetwork()
let fullTxList = this.getFullTxList() const fullTxList = this.getFullTxList()
return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network)
} }
@ -64,10 +69,10 @@ module.exports = class TransactionManager extends EventEmitter {
// Adds a tx to the txlist // Adds a tx to the txlist
addTx (txMeta) { addTx (txMeta) {
let txCount = this.getTxCount() const txCount = this.getTxCount()
let network = this.getNetwork() const network = this.getNetwork()
let fullTxList = this.getFullTxList() const fullTxList = this.getFullTxList()
let txHistoryLimit = this.txHistoryLimit const txHistoryLimit = this.txHistoryLimit
// checks if the length of the tx history is // checks if the length of the tx history is
// longer then desired persistence limit // longer then desired persistence limit
@ -197,7 +202,7 @@ module.exports = class TransactionManager extends EventEmitter {
} }
fillInTxParams (txId, cb) { fillInTxParams (txId, cb) {
let txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => {
if (err) return cb(err) if (err) return cb(err)
this.updateTx(txMeta) this.updateTx(txMeta)
@ -205,7 +210,7 @@ module.exports = class TransactionManager extends EventEmitter {
}) })
} }
getChainId() { getChainId () {
const networkState = this.networkStore.getState() const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState.network) const getChainId = parseInt(networkState.network)
if (Number.isNaN(getChainId)) { if (Number.isNaN(getChainId)) {
@ -230,7 +235,11 @@ module.exports = class TransactionManager extends EventEmitter {
}) })
} }
publishTransaction (txId, rawTx, cb) { publishTransaction (txId, rawTx, cb = warn) {
const txMeta = this.getTx(txId)
txMeta.rawTx = rawTx
this.updateTx(txMeta)
this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => {
if (err) return cb(err) if (err) return cb(err)
this.setTxHash(txId, txHash) this.setTxHash(txId, txHash)
@ -242,7 +251,7 @@ module.exports = class TransactionManager extends EventEmitter {
// receives a txHash records the tx as signed // receives a txHash records the tx as signed
setTxHash (txId, txHash) { setTxHash (txId, txHash) {
// Add the tx hash to the persisted meta-tx object // Add the tx hash to the persisted meta-tx object
let txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.hash = txHash txMeta.hash = txHash
this.updateTx(txMeta) this.updateTx(txMeta)
} }
@ -315,7 +324,7 @@ module.exports = class TransactionManager extends EventEmitter {
} }
setTxStatusFailed (txId, reason) { setTxStatusFailed (txId, reason) {
let txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.err = reason txMeta.err = reason
this.updateTx(txMeta) this.updateTx(txMeta)
this._setTxStatus(txId, 'failed') this._setTxStatus(txId, 'failed')
@ -338,7 +347,7 @@ module.exports = class TransactionManager extends EventEmitter {
var txHash = txMeta.hash var txHash = txMeta.hash
var txId = txMeta.id var txId = txMeta.id
if (!txHash) { if (!txHash) {
let errReason = { const errReason = {
errCode: 'No hash was provided', errCode: 'No hash was provided',
message: 'We had an error while submitting this transaction, please try again.', message: 'We had an error while submitting this transaction, please try again.',
} }
@ -353,7 +362,7 @@ module.exports = class TransactionManager extends EventEmitter {
message: 'There was a problem loading this transaction.', message: 'There was a problem loading this transaction.',
} }
this.updateTx(txMeta) this.updateTx(txMeta)
return console.error(err) return log.error(err)
} }
if (txParams.blockNumber) { if (txParams.blockNumber) {
this.setTxStatusConfirmed(txId) this.setTxStatusConfirmed(txId)
@ -380,6 +389,7 @@ module.exports = class TransactionManager extends EventEmitter {
this.emit(`${txMeta.id}:${status}`, txId) this.emit(`${txMeta.id}:${status}`, txId)
if (status === 'submitted' || status === 'rejected') { if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta) this.emit(`${txMeta.id}:finished`, txMeta)
} }
this.updateTx(txMeta) this.updateTx(txMeta)
this.emit('updateBadge') this.emit('updateBadge')
@ -399,7 +409,47 @@ module.exports = class TransactionManager extends EventEmitter {
}) })
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
} }
continuallyResubmitPendingTxs () {
const pending = this.getTxsByMetaData('status', 'submitted')
const resubmit = denodeify(this.resubmitTx.bind(this))
Promise.all(pending.map(txMeta => resubmit(txMeta)))
.catch((reason) => {
log.info('Problem resubmitting tx', reason)
})
.then(() => {
global.setTimeout(() => {
this.continuallyResubmitPendingTxs()
}, RESUBMIT_INTERVAL)
})
}
resubmitTx (txMeta, cb) {
// Increment a try counter.
if (!('retryCount' in txMeta)) {
txMeta.retryCount = 0
}
// Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) {
return cb()
}
if (txMeta.retryCount > RETRY_LIMIT) {
txMeta.err = {
isWarning: true,
message: 'Gave up submitting tx.',
}
this.updateTx(txMeta)
return log.error(txMeta.err.message)
}
txMeta.retryCount++
const rawTx = txMeta.rawTx
this.txProviderUtils.publishTransaction(rawTx, cb)
}
} }
const warn = () => console.warn('warn was used no cb provided') const warn = () => log.warn('warn was used no cb provided')

@ -1,11 +1,15 @@
// test and development environment variables
const env = process.env.METAMASK_ENV
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
// //
// The default state of MetaMask // The default state of MetaMask
// //
module.exports = { module.exports = {
config: { config: {},
NetworkController: {
provider: { provider: {
type: 'testnet', type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet',
}, },
}, },
} }

@ -31,26 +31,11 @@ web3.setProvider = function () {
console.log('MetaMask - overrode web3.setProvider') console.log('MetaMask - overrode web3.setProvider')
} }
console.log('MetaMask - injected web3') console.log('MetaMask - injected web3')
// export global web3, with usage-detection reload fn // export global web3, with usage-detection
var triggerReload = setupDappAutoReload(web3) setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// listen for reset requests from metamask
var reloadStream = inpageProvider.multiStream.createStream('reload')
reloadStream.once('data', triggerReload)
// setup ping timeout autoreload
// LocalMessageDuplexStream does not self-close, so reload if pingStream fails
// var pingChannel = inpageProvider.multiStream.createStream('pingpong')
// var pingStream = new PingStream({ objectMode: true })
// wait for first successful reponse
// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully
// metamaskStream.once('data', function(){
// pingStream.pipe(pingChannel).pipe(pingStream)
// })
// endOfStream(pingStream, triggerReload)
// set web3 defaultAccount // set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) { inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress web3.eth.defaultAccount = state.selectedAddress
}) })

@ -187,7 +187,7 @@ class KeyringController extends EventEmitter {
.then((accounts) => { .then((accounts) => {
switch (type) { switch (type) {
case 'Simple Key Pair': case 'Simple Key Pair':
let isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0]))
return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate'))
default: default:
return Promise.resolve(newAccount) return Promise.resolve(newAccount)
@ -582,7 +582,7 @@ class KeyringController extends EventEmitter {
}) })
} }
_updateMemStoreKeyrings() { _updateMemStoreKeyrings () {
Promise.all(this.keyrings.map(this.displayForKeyring)) Promise.all(this.keyrings.map(this.displayForKeyring))
.then((keyrings) => { .then((keyrings) => {
this.memStore.updateState({ keyrings }) this.memStore.updateState({ keyrings })

@ -1,30 +1,33 @@
const once = require('once')
const ensnare = require('ensnare')
module.exports = setupDappAutoReload module.exports = setupDappAutoReload
function setupDappAutoReload (web3) { function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage // export web3 as a global, checking for usage
var pageIsUsingWeb3 = false global.web3 = new Proxy(web3, {
var resetWasRequested = false get: (_web3, name) => {
global.web3 = ensnare(web3, once(function () { // get the time of use
// if web3 usage happened after a reset request, trigger reset late if (name !== '_used') _web3._used = Date.now()
if (resetWasRequested) return triggerReset() return _web3[name]
// mark web3 as used },
pageIsUsingWeb3 = true set: (_web3, name, value) => {
// reset web3 reference _web3[name] = value
global.web3 = web3 },
})) })
var networkVersion
return handleResetRequest observable.subscribe(function (state) {
// get the initial network
const curentNetVersion = state.networkVersion
if (!networkVersion) networkVersion = curentNetVersion
function handleResetRequest () { if (curentNetVersion !== networkVersion && web3._used) {
resetWasRequested = true const timeSinceUse = Date.now() - web3._used
// ignore if web3 was not used // if web3 was recently used then delay the reloading of the page
if (!pageIsUsingWeb3) return timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500)
// reload after short timeout // prevent reentry into if statement if state updates again before
setTimeout(triggerReset, 500) // reload
} networkVersion = curentNetVersion
}
})
} }
// reload the page // reload the page

@ -1,6 +1,6 @@
module.exports = getBuyEthUrl module.exports = getBuyEthUrl
function getBuyEthUrl({ network, amount, address }){ function getBuyEthUrl ({ network, amount, address }) {
let url let url
switch (network) { switch (network) {
case '1': case '1':
@ -11,9 +11,13 @@ function getBuyEthUrl({ network, amount, address }){
url = 'https://faucet.metamask.io/' url = 'https://faucet.metamask.io/'
break break
case '4':
url = 'https://www.rinkeby.io/'
break
case '42': case '42':
url = 'https://github.com/kovan-testnet/faucet' url = 'https://github.com/kovan-testnet/faucet'
break break
} }
return url return url
} }

@ -1,11 +1,12 @@
const MetamaskConfig = require('../config.js')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const normalize = require('eth-sig-util').normalize const normalize = require('eth-sig-util').normalize
const MetamaskConfig = require('../config.js')
const TESTNET_RPC = MetamaskConfig.network.testnet
const MAINNET_RPC = MetamaskConfig.network.mainnet const MAINNET_RPC = MetamaskConfig.network.mainnet
const MORDEN_RPC = MetamaskConfig.network.morden const ROPSTEN_RPC = MetamaskConfig.network.ropsten
const KOVAN_RPC = MetamaskConfig.network.kovan const KOVAN_RPC = MetamaskConfig.network.kovan
const RINKEBY_RPC = MetamaskConfig.network.rinkeby
/* The config-manager is a convenience object /* The config-manager is a convenience object
* wrapping a pojo-migrator. * wrapping a pojo-migrator.
@ -33,36 +34,6 @@ ConfigManager.prototype.getConfig = function () {
return data.config return data.config
} }
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
var config = this.getConfig()
config.provider = {
type: 'rpc',
rpcTarget: rpcUrl,
}
this.setConfig(config)
}
ConfigManager.prototype.setProviderType = function (type) {
var config = this.getConfig()
config.provider = {
type: type,
}
this.setConfig(config)
}
ConfigManager.prototype.useEtherscanProvider = function () {
var config = this.getConfig()
config.provider = {
type: 'etherscan',
}
this.setConfig(config)
}
ConfigManager.prototype.getProvider = function () {
var config = this.getConfig()
return config.provider
}
ConfigManager.prototype.setData = function (data) { ConfigManager.prototype.setData = function (data) {
this.store.putState(data) this.store.putState(data)
} }
@ -136,6 +107,35 @@ ConfigManager.prototype.getSeedWords = function () {
var data = this.getData() var data = this.getData()
return data.seedWords return data.seedWords
} }
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
var config = this.getConfig()
config.provider = {
type: 'rpc',
rpcTarget: rpcUrl,
}
this.setConfig(config)
}
ConfigManager.prototype.setProviderType = function (type) {
var config = this.getConfig()
config.provider = {
type: type,
}
this.setConfig(config)
}
ConfigManager.prototype.useEtherscanProvider = function () {
var config = this.getConfig()
config.provider = {
type: 'etherscan',
}
this.setConfig(config)
}
ConfigManager.prototype.getProvider = function () {
var config = this.getConfig()
return config.provider
}
ConfigManager.prototype.getCurrentRpcAddress = function () { ConfigManager.prototype.getCurrentRpcAddress = function () {
var provider = this.getProvider() var provider = this.getProvider()
@ -145,17 +145,17 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
case 'mainnet': case 'mainnet':
return MAINNET_RPC return MAINNET_RPC
case 'testnet': case 'ropsten':
return TESTNET_RPC return ROPSTEN_RPC
case 'morden':
return MORDEN_RPC
case 'kovan': case 'kovan':
return KOVAN_RPC return KOVAN_RPC
case 'rinkeby':
return RINKEBY_RPC
default: default:
return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC
} }
} }

@ -10,7 +10,7 @@
const async = require('async') const async = require('async')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
function noop() {} function noop () {}
class EthereumStore extends ObservableStore { class EthereumStore extends ObservableStore {
@ -21,6 +21,7 @@ class EthereumStore extends ObservableStore {
transactions: {}, transactions: {},
currentBlockNumber: '0', currentBlockNumber: '0',
currentBlockHash: '', currentBlockHash: '',
currentBlockGasLimit: '',
}) })
this._provider = opts.provider this._provider = opts.provider
this._query = new EthQuery(this._provider) this._query = new EthQuery(this._provider)
@ -73,6 +74,7 @@ class EthereumStore extends ObservableStore {
this._currentBlockNumber = blockNumber this._currentBlockNumber = blockNumber
this.updateState({ currentBlockNumber: parseInt(blockNumber) }) this.updateState({ currentBlockNumber: parseInt(blockNumber) })
this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`})
this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` })
async.parallel([ async.parallel([
this._updateAccounts.bind(this), this._updateAccounts.bind(this),
this._updateTransactions.bind(this, blockNumber), this._updateTransactions.bind(this, blockNumber),

@ -34,6 +34,7 @@ function MetamaskInpageProvider (connectionStream) {
asyncProvider, asyncProvider,
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
) )
// start and stop polling to unblock first block lock
self.idMap = {} self.idMap = {}
// handle sendAsync requests via asyncProvider // handle sendAsync requests via asyncProvider
@ -85,7 +86,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
break break
case 'net_version': case 'net_version':
let networkVersion = self.publicConfigStore.getState().networkVersion const networkVersion = self.publicConfigStore.getState().networkVersion
result = networkVersion result = networkVersion
break break
@ -125,7 +126,7 @@ function eachJsonMessage (payload, transformFn) {
} }
} }
function logStreamDisconnectWarning(remoteLabel, err){ function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg) console.warn(warningMsg)

@ -4,7 +4,7 @@ const ethUtil = require('ethereumjs-util')
const createId = require('./random-id') const createId = require('./random-id')
module.exports = class MessageManager extends EventEmitter{ module.exports = class MessageManager extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.memStore = new ObservableStore({ this.memStore = new ObservableStore({
@ -108,7 +108,7 @@ module.exports = class MessageManager extends EventEmitter{
} }
function normalizeMsgData(data) { function normalizeMsgData (data) {
if (data.slice(0, 2) === '0x') { if (data.slice(0, 2) === '0x') {
// data is already hex // data is already hex
return data return data

@ -1,42 +1,35 @@
const asyncQ = require('async-q')
class Migrator { class Migrator {
constructor (opts = {}) { constructor (opts = {}) {
let migrations = opts.migrations || [] const migrations = opts.migrations || []
// sort migrations by version
this.migrations = migrations.sort((a, b) => a.version - b.version) this.migrations = migrations.sort((a, b) => a.version - b.version)
let lastMigration = this.migrations.slice(-1)[0] // grab migration with highest version
const lastMigration = this.migrations.slice(-1)[0]
// use specified defaultVersion or highest migration version // use specified defaultVersion or highest migration version
this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0 this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0
} }
// run all pending migrations on meta in place // run all pending migrations on meta in place
migrateData (versionedData = this.generateInitialState()) { async migrateData (versionedData = this.generateInitialState()) {
let remaining = this.migrations.filter(migrationIsPending) const pendingMigrations = this.migrations.filter(migrationIsPending)
return ( for (const index in pendingMigrations) {
asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration)) const migration = pendingMigrations[index]
.then(() => versionedData) versionedData = await migration.migrate(versionedData)
) if (!versionedData.data) throw new Error('Migrator - migration returned empty data')
if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
// migration is "pending" if hit has a higher }
return versionedData
// migration is "pending" if it has a higher
// version number than currentVersion // version number than currentVersion
function migrationIsPending(migration) { function migrationIsPending (migration) {
return migration.version > versionedData.meta.version return migration.version > versionedData.meta.version
} }
} }
runMigration(versionedData, migration) {
return (
migration.migrate(versionedData)
.then((versionedData) => {
if (!versionedData.data) return Promise.reject(new Error('Migrator - Migration returned empty data'))
if (migration.version !== undefined && versionedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly'))
return Promise.resolve(versionedData)
})
)
}
generateInitialState (initState) { generateInitialState (initState) {
return { return {
meta: { meta: {

@ -24,9 +24,6 @@ class NotificationManager {
width, width,
height, height,
}) })
.catch((reason) => {
log.error('failed to create poupup', reason)
})
} }
}) })
} }
@ -71,4 +68,4 @@ class NotificationManager {
} }
module.exports = NotificationManager module.exports = NotificationManager

@ -5,7 +5,7 @@ const createId = require('./random-id')
const hexRe = /^[0-9A-Fa-f]+$/g const hexRe = /^[0-9A-Fa-f]+$/g
module.exports = class PersonalMessageManager extends EventEmitter{ module.exports = class PersonalMessageManager extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.memStore = new ObservableStore({ this.memStore = new ObservableStore({
@ -108,7 +108,7 @@ module.exports = class PersonalMessageManager extends EventEmitter{
this.emit('updateBadge') this.emit('updateBadge')
} }
normalizeMsgData(data) { normalizeMsgData (data) {
try { try {
const stripped = ethUtil.stripHexPrefix(data) const stripped = ethUtil.stripHexPrefix(data)
if (stripped.match(hexRe)) { if (stripped.match(hexRe)) {

@ -1,5 +1,4 @@
const async = require('async') const async = require('async')
const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx') const Transaction = require('ethereumjs-tx')
const normalize = require('eth-sig-util').normalize const normalize = require('eth-sig-util').normalize
@ -7,15 +6,14 @@ const BN = ethUtil.BN
/* /*
tx-utils are utility methods for Transaction manager tx-utils are utility methods for Transaction manager
its passed a provider and that is passed to ethquery its passed ethquery
and used to do things like calculate gas of a tx. and used to do things like calculate gas of a tx.
*/ */
module.exports = class txProviderUtils { module.exports = class txProviderUtils {
constructor (provider) { constructor (ethQuery) {
this.provider = provider this.query = ethQuery
this.query = new EthQuery(provider)
} }
analyzeGasUsage (txMeta, cb) { analyzeGasUsage (txMeta, cb) {
@ -35,7 +33,9 @@ module.exports = class txProviderUtils {
txMeta.gasLimitSpecified = Boolean(txParams.gas) txMeta.gasLimitSpecified = Boolean(txParams.gas)
// if not, fallback to block gasLimit // if not, fallback to block gasLimit
if (!txMeta.gasLimitSpecified) { if (!txMeta.gasLimitSpecified) {
txParams.gas = blockGasLimitHex const blockGasLimitBN = hexToBn(blockGasLimitHex)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
txParams.gas = bnToHex(saferGasLimitBN)
} }
// run tx, see if it will OOG // run tx, see if it will OOG
this.query.estimateGas(txParams, cb) this.query.estimateGas(txParams, cb)
@ -75,14 +75,14 @@ module.exports = class txProviderUtils {
} }
fillInTxParams (txParams, cb) { fillInTxParams (txParams, cb) {
let fromAddress = txParams.from const fromAddress = txParams.from
let reqs = {} const reqs = {}
if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb) if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb)
if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb) if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb)
if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb) if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb)
async.parallel(reqs, function(err, result) { async.parallel(reqs, function (err, result) {
if (err) return cb(err) if (err) return cb(err)
// write results to txParams obj // write results to txParams obj
Object.assign(txParams, result) Object.assign(txParams, result)
@ -123,14 +123,20 @@ module.exports = class txProviderUtils {
// util // util
function isUndef(value) { function isUndef (value) {
return value === undefined return value === undefined
} }
function bnToHex(inputBn) { function bnToHex (inputBn) {
return ethUtil.addHexPrefix(inputBn.toString(16)) return ethUtil.addHexPrefix(inputBn.toString(16))
} }
function hexToBn(inputHex) { function hexToBn (inputHex) {
return new BN(ethUtil.stripHexPrefix(inputHex), 16) return new BN(ethUtil.stripHexPrefix(inputHex), 16)
} }
function BnMultiplyByFraction (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}

@ -4,13 +4,12 @@ const promiseToCallback = require('promise-to-callback')
const pipe = require('pump') const pipe = require('pump')
const Dnode = require('dnode') const Dnode = require('dnode')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const storeTransform = require('obs-store/lib/transform')
const EthStore = require('./lib/eth-store') const EthStore = require('./lib/eth-store')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const streamIntoProvider = require('web3-stream-provider/handler') const streamIntoProvider = require('web3-stream-provider/handler')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const KeyringController = require('./keyring-controller') const KeyringController = require('./keyring-controller')
const NetworkController = require('./controllers/network')
const PreferencesController = require('./controllers/preferences') const PreferencesController = require('./controllers/preferences')
const CurrencyController = require('./controllers/currency') const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller') const NoticeController = require('./notice-controller')
@ -18,7 +17,7 @@ const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book') const AddressBookController = require('./controllers/address-book')
const MessageManager = require('./lib/message-manager') const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager') const PersonalMessageManager = require('./lib/personal-message-manager')
const TxManager = require('./transaction-manager') const TransactionController = require('./controllers/transactions')
const ConfigManager = require('./lib/config-manager') const ConfigManager = require('./lib/config-manager')
const autoFaucet = require('./lib/auto-faucet') const autoFaucet = require('./lib/auto-faucet')
const nodeify = require('./lib/nodeify') const nodeify = require('./lib/nodeify')
@ -32,7 +31,7 @@ module.exports = class MetamaskController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.opts = opts this.opts = opts
let initState = opts.initState || {} const initState = opts.initState || {}
// platform-specific api // platform-specific api
this.platform = opts.platform this.platform = opts.platform
@ -41,8 +40,8 @@ module.exports = class MetamaskController extends EventEmitter {
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
// network store // network store
this.networkStore = new ObservableStore({ network: 'loading' })
this.networkController = new NetworkController(initState.NetworkController)
// config manager // config manager
this.configManager = new ConfigManager({ this.configManager = new ConfigManager({
store: this.store, store: this.store,
@ -62,8 +61,6 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
this.provider.on('block', this.logBlock.bind(this))
this.provider.on('error', this.verifyNetwork.bind(this))
// eth data query tools // eth data query tools
this.ethQuery = new EthQuery(this.provider) this.ethQuery = new EthQuery(this.provider)
@ -76,7 +73,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.keyringController = new KeyringController({ this.keyringController = new KeyringController({
initState: initState.KeyringController, initState: initState.KeyringController,
ethStore: this.ethStore, ethStore: this.ethStore,
getNetwork: this.getNetworkState.bind(this), getNetwork: this.networkController.getNetworkState.bind(this.networkController),
}) })
this.keyringController.on('newAccount', (address) => { this.keyringController.on('newAccount', (address) => {
this.preferencesController.setSelectedAddress(address) this.preferencesController.setSelectedAddress(address)
@ -91,15 +88,16 @@ module.exports = class MetamaskController extends EventEmitter {
}, this.keyringController) }, this.keyringController)
// tx mgmt // tx mgmt
this.txManager = new TxManager({ this.txController = new TransactionController({
initState: initState.TransactionManager, initState: initState.TransactionController || initState.TransactionManager,
networkStore: this.networkStore, networkStore: this.networkController.networkStore,
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
txHistoryLimit: 40, txHistoryLimit: 40,
getNetwork: this.getNetworkState.bind(this), getNetwork: this.networkController.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController), signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider, provider: this.provider,
blockTracker: this.provider, blockTracker: this.provider,
ethQuery: this.ethQuery,
}) })
// notices // notices
@ -114,14 +112,14 @@ module.exports = class MetamaskController extends EventEmitter {
initState: initState.ShapeShiftController, initState: initState.ShapeShiftController,
}) })
this.lookupNetwork() this.networkController.lookupNetwork()
this.messageManager = new MessageManager() this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager() this.personalMessageManager = new PersonalMessageManager()
this.publicConfigStore = this.initPublicConfigStore() this.publicConfigStore = this.initPublicConfigStore()
// manual disk state subscriptions // manual disk state subscriptions
this.txManager.store.subscribe((state) => { this.txController.store.subscribe((state) => {
this.store.updateState({ TransactionManager: state }) this.store.updateState({ TransactionController: state })
}) })
this.keyringController.store.subscribe((state) => { this.keyringController.store.subscribe((state) => {
this.store.updateState({ KeyringController: state }) this.store.updateState({ KeyringController: state })
@ -141,11 +139,14 @@ module.exports = class MetamaskController extends EventEmitter {
this.shapeshiftController.store.subscribe((state) => { this.shapeshiftController.store.subscribe((state) => {
this.store.updateState({ ShapeShiftController: state }) this.store.updateState({ ShapeShiftController: state })
}) })
this.networkController.store.subscribe((state) => {
this.store.updateState({ NetworkController: state })
})
// manual mem state subscriptions // manual mem state subscriptions
this.networkStore.subscribe(this.sendUpdate.bind(this)) this.networkController.store.subscribe(this.sendUpdate.bind(this))
this.ethStore.subscribe(this.sendUpdate.bind(this)) this.ethStore.subscribe(this.sendUpdate.bind(this))
this.txManager.memStore.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(this.sendUpdate.bind(this))
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
@ -161,17 +162,21 @@ module.exports = class MetamaskController extends EventEmitter {
// //
initializeProvider () { initializeProvider () {
return this.networkController.initializeProvider({
let provider = MetaMaskProvider({
static: { static: {
eth_syncing: false, eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`, web3_clientVersion: `MetaMask/v${version}`,
}, },
rpcUrl: this.configManager.getCurrentRpcAddress(), rpcUrl: this.networkController.getCurrentRpcAddress(),
// account mgmt // account mgmt
getAccounts: (cb) => { getAccounts: (cb) => {
let selectedAddress = this.preferencesController.getSelectedAddress() const isUnlocked = this.keyringController.memStore.getState().isUnlocked
let result = selectedAddress ? [selectedAddress] : [] const result = []
const selectedAddress = this.preferencesController.getSelectedAddress()
// only show address if account is unlocked
if (isUnlocked && selectedAddress) {
result.push(selectedAddress)
}
cb(null, result) cb(null, result)
}, },
// tx signing // tx signing
@ -182,26 +187,23 @@ module.exports = class MetamaskController extends EventEmitter {
// new style msg signing // new style msg signing
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
}) })
return provider
} }
initPublicConfigStore () { initPublicConfigStore () {
// get init state // get init state
const publicConfigStore = new ObservableStore() const publicConfigStore = new ObservableStore()
// sync publicConfigStore with transform // memStore -> transform -> publicConfigStore
pipe( this.on('update', (memState) => {
this.store, const publicState = selectPublicState(memState)
storeTransform(selectPublicState.bind(this)), publicConfigStore.putState(publicState)
publicConfigStore })
)
function selectPublicState(state) { function selectPublicState (memState) {
const result = { selectedAddress: undefined } const result = {
try { selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined,
result.selectedAddress = state.PreferencesController.selectedAddress networkVersion: memState.network,
result.networkVersion = this.getNetworkState() }
} catch (_) {}
return result return result
} }
@ -220,9 +222,9 @@ module.exports = class MetamaskController extends EventEmitter {
{ {
isInitialized, isInitialized,
}, },
this.networkStore.getState(), this.networkController.store.getState(),
this.ethStore.getState(), this.ethStore.getState(),
this.txManager.memStore.getState(), this.txController.memStore.getState(),
this.messageManager.memStore.getState(), this.messageManager.memStore.getState(),
this.personalMessageManager.memStore.getState(), this.personalMessageManager.memStore.getState(),
this.keyringController.memStore.getState(), this.keyringController.memStore.getState(),
@ -247,62 +249,61 @@ module.exports = class MetamaskController extends EventEmitter {
getApi () { getApi () {
const keyringController = this.keyringController const keyringController = this.keyringController
const preferencesController = this.preferencesController const preferencesController = this.preferencesController
const txManager = this.txManager const txController = this.txController
const noticeController = this.noticeController const noticeController = this.noticeController
const addressBookController = this.addressBookController const addressBookController = this.addressBookController
return { return {
// etc // etc
getState: (cb) => cb(null, this.getState()), getState: (cb) => cb(null, this.getState()),
setProviderType: this.setProviderType.bind(this), setProviderType: this.networkController.setProviderType.bind(this.networkController),
useEtherscanProvider: this.useEtherscanProvider.bind(this), setCurrentCurrency: this.setCurrentCurrency.bind(this),
setCurrentCurrency: this.setCurrentCurrency.bind(this), markAccountsFound: this.markAccountsFound.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
// coinbase // coinbase
buyEth: this.buyEth.bind(this), buyEth: this.buyEth.bind(this),
// shapeshift // shapeshift
createShapeShiftTx: this.createShapeShiftTx.bind(this), createShapeShiftTx: this.createShapeShiftTx.bind(this),
// primary HD keyring management // primary HD keyring management
addNewAccount: this.addNewAccount.bind(this), addNewAccount: this.addNewAccount.bind(this),
placeSeedWords: this.placeSeedWords.bind(this), placeSeedWords: this.placeSeedWords.bind(this),
clearSeedWordCache: this.clearSeedWordCache.bind(this), clearSeedWordCache: this.clearSeedWordCache.bind(this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this), importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
// vault management // vault management
submitPassword: this.submitPassword.bind(this), submitPassword: this.submitPassword.bind(this),
// PreferencesController // PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this), setCustomRpc: nodeify(this.setCustomRpc).bind(this),
// AddressController // AddressController
setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController), setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController),
// KeyringController // KeyringController
setLocked: nodeify(keyringController.setLocked).bind(keyringController), setLocked: nodeify(keyringController.setLocked).bind(keyringController),
createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController), createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController), createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController),
addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController), addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController), saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
// txManager // txController
approveTransaction: txManager.approveTransaction.bind(txManager), approveTransaction: txController.approveTransaction.bind(txController),
cancelTransaction: txManager.cancelTransaction.bind(txManager), cancelTransaction: txController.cancelTransaction.bind(txController),
updateAndApproveTransaction: this.updateAndApproveTx.bind(this), updateAndApproveTransaction: this.updateAndApproveTx.bind(this),
// messageManager // messageManager
signMessage: nodeify(this.signMessage).bind(this), signMessage: nodeify(this.signMessage).bind(this),
cancelMessage: this.cancelMessage.bind(this), cancelMessage: this.cancelMessage.bind(this),
// personalMessageManager // personalMessageManager
signPersonalMessage: nodeify(this.signPersonalMessage).bind(this), signPersonalMessage: nodeify(this.signPersonalMessage).bind(this),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this), cancelPersonalMessage: this.cancelPersonalMessage.bind(this),
// notices // notices
checkNotices: noticeController.updateNoticesList.bind(noticeController), checkNotices: noticeController.updateNoticesList.bind(noticeController),
markNoticeRead: noticeController.markNoticeRead.bind(noticeController), markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
} }
} }
@ -342,9 +343,7 @@ module.exports = class MetamaskController extends EventEmitter {
console.error('Error in RPC response:\n', response.error) console.error('Error in RPC response:\n', response.error)
} }
if (request.isMetamaskInternal) return if (request.isMetamaskInternal) return
if (global.METAMASK_DEBUG) { log.info(`RPC (${originDomain}):`, request, '->', response)
console.log(`RPC (${originDomain}):`, request, '->', response)
}
} }
} }
@ -422,12 +421,12 @@ module.exports = class MetamaskController extends EventEmitter {
newUnapprovedTransaction (txParams, cb) { newUnapprovedTransaction (txParams, cb) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const self = this const self = this
self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => { self.txController.addUnapprovedTransaction(txParams, (err, txMeta) => {
if (err) return cb(err) if (err) return cb(err)
self.sendUpdate() self.sendUpdate()
self.opts.showUnapprovedTx(txMeta) self.opts.showUnapprovedTx(txMeta)
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
self.txManager.once(`${txMeta.id}:finished`, (completedTx) => { self.txController.once(`${txMeta.id}:finished`, (completedTx) => {
switch (completedTx.status) { switch (completedTx.status) {
case 'submitted': case 'submitted':
return cb(null, completedTx.hash) return cb(null, completedTx.hash)
@ -441,7 +440,7 @@ module.exports = class MetamaskController extends EventEmitter {
} }
newUnsignedMessage (msgParams, cb) { newUnsignedMessage (msgParams, cb) {
let msgId = this.messageManager.addUnapprovedMessage(msgParams) const msgId = this.messageManager.addUnapprovedMessage(msgParams)
this.sendUpdate() this.sendUpdate()
this.opts.showUnconfirmedMessage() this.opts.showUnconfirmedMessage()
this.messageManager.once(`${msgId}:finished`, (data) => { this.messageManager.once(`${msgId}:finished`, (data) => {
@ -461,7 +460,7 @@ module.exports = class MetamaskController extends EventEmitter {
return cb(new Error('MetaMask Message Signature: from field is required.')) return cb(new Error('MetaMask Message Signature: from field is required.'))
} }
let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
this.sendUpdate() this.sendUpdate()
this.opts.showUnconfirmedMessage() this.opts.showUnconfirmedMessage()
this.personalMessageManager.once(`${msgId}:finished`, (data) => { this.personalMessageManager.once(`${msgId}:finished`, (data) => {
@ -476,11 +475,11 @@ module.exports = class MetamaskController extends EventEmitter {
}) })
} }
updateAndApproveTx(txMeta, cb) { updateAndApproveTx (txMeta, cb) {
log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`) log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`)
const txManager = this.txManager const txController = this.txController
txManager.updateTx(txMeta) txController.updateTx(txMeta)
txManager.approveTransaction(txMeta.id, cb) txController.approveTransaction(txMeta.id, cb)
} }
signMessage (msgParams, cb) { signMessage (msgParams, cb) {
@ -502,7 +501,7 @@ module.exports = class MetamaskController extends EventEmitter {
}) })
} }
cancelMessage(msgId, cb) { cancelMessage (msgId, cb) {
const messageManager = this.messageManager const messageManager = this.messageManager
messageManager.rejectMsg(msgId) messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') { if (cb && typeof cb === 'function') {
@ -512,7 +511,7 @@ module.exports = class MetamaskController extends EventEmitter {
// Prefixed Style Message Signing Methods: // Prefixed Style Message Signing Methods:
approvePersonalMessage (msgParams, cb) { approvePersonalMessage (msgParams, cb) {
let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
this.sendUpdate() this.sendUpdate()
this.opts.showUnconfirmedMessage() this.opts.showUnconfirmedMessage()
this.personalMessageManager.once(`${msgId}:finished`, (data) => { this.personalMessageManager.once(`${msgId}:finished`, (data) => {
@ -545,7 +544,7 @@ module.exports = class MetamaskController extends EventEmitter {
}) })
} }
cancelPersonalMessage(msgId, cb) { cancelPersonalMessage (msgId, cb) {
const messageManager = this.personalMessageManager const messageManager = this.personalMessageManager
messageManager.rejectMsg(msgId) messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') { if (cb && typeof cb === 'function') {
@ -559,13 +558,13 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, this.getState()) cb(null, this.getState())
} }
restoreOldVaultAccounts(migratorOutput) { restoreOldVaultAccounts (migratorOutput) {
const { serialized } = migratorOutput const { serialized } = migratorOutput
return this.keyringController.restoreKeyring(serialized) return this.keyringController.restoreKeyring(serialized)
.then(() => migratorOutput) .then(() => migratorOutput)
} }
restoreOldLostAccounts(migratorOutput) { restoreOldLostAccounts (migratorOutput) {
const { lostAccounts } = migratorOutput const { lostAccounts } = migratorOutput
if (lostAccounts) { if (lostAccounts) {
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address)) this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
@ -591,12 +590,6 @@ module.exports = class MetamaskController extends EventEmitter {
// //
// Log blocks // Log blocks
logBlock (block) {
if (global.METAMASK_DEBUG) {
console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
}
this.verifyNetwork()
}
setCurrentCurrency (currencyCode, cb) { setCurrentCurrency (currencyCode, cb) {
try { try {
@ -615,7 +608,7 @@ module.exports = class MetamaskController extends EventEmitter {
buyEth (address, amount) { buyEth (address, amount) {
if (!amount) amount = '5' if (!amount) amount = '5'
const network = this.getNetworkState() const network = this.networkController.getNetworkState()
const url = getBuyEthUrl({ network, address, amount }) const url = getBuyEthUrl({ network, address, amount })
if (url) this.platform.openWindow({ url }) if (url) this.platform.openWindow({ url })
} }
@ -623,71 +616,21 @@ module.exports = class MetamaskController extends EventEmitter {
createShapeShiftTx (depositAddress, depositType) { createShapeShiftTx (depositAddress, depositType) {
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
} }
// network
//
// network
//
verifyNetwork () {
// Check network when restoring connectivity:
if (this.isNetworkLoading()) this.lookupNetwork()
}
setDefaultRpc () { setDefaultRpc () {
this.configManager.setRpcTarget('http://localhost:8545') this.networkController.setRpcTarget('http://localhost:8545')
this.platform.reload()
this.lookupNetwork()
return Promise.resolve('http://localhost:8545') return Promise.resolve('http://localhost:8545')
} }
setCustomRpc (rpcTarget, rpcList) { setCustomRpc (rpcTarget, rpcList) {
this.configManager.setRpcTarget(rpcTarget) this.networkController.setRpcTarget(rpcTarget)
return this.preferencesController.updateFrequentRpcList(rpcTarget)
.then(() => {
this.platform.reload()
this.lookupNetwork()
return Promise.resolve(rpcTarget)
})
}
setProviderType (type) {
this.configManager.setProviderType(type)
this.platform.reload()
this.lookupNetwork()
}
useEtherscanProvider () {
this.configManager.useEtherscanProvider()
this.platform.reload()
}
getNetworkState () {
return this.networkStore.getState().network
}
setNetworkState (network) {
return this.networkStore.updateState({ network })
}
isNetworkLoading () { return this.preferencesController.updateFrequentRpcList(rpcTarget)
return this.getNetworkState() === 'loading' .then(() => {
} return Promise.resolve(rpcTarget)
lookupNetwork (err) {
if (err) {
this.setNetworkState('loading')
}
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) {
this.setNetworkState('loading')
return
}
if (global.METAMASK_DEBUG) {
console.log('web3.getNetwork returned ' + network)
}
this.setNetworkState(network)
}) })
} }
} }

@ -7,7 +7,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
if (versionedData.data.config.provider.type === 'etherscan') { if (versionedData.data.config.provider.type === 'etherscan') {

@ -8,7 +8,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
if (versionedData.data.config.provider.rpcTarget === oldTestRpc) { if (versionedData.data.config.provider.rpcTarget === oldTestRpc) {

@ -6,7 +6,7 @@ module.exports = {
version, version,
migrate: function (versionedData) { migrate: function (versionedData) {
let safeVersionedData = clone(versionedData) const safeVersionedData = clone(versionedData)
safeVersionedData.meta.version = version safeVersionedData.meta.version = version
try { try {
if (safeVersionedData.data.config.provider.type !== 'rpc') return Promise.resolve(safeVersionedData) if (safeVersionedData.data.config.provider.type !== 'rpc') return Promise.resolve(safeVersionedData)

@ -14,7 +14,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -13,7 +13,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -13,7 +13,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -13,7 +13,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -13,7 +13,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -13,7 +13,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -12,7 +12,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -12,7 +12,7 @@ module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: function (originalVersionedData) {
let versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { try {
const state = versionedData.data const state = versionedData.data

@ -0,0 +1,34 @@
const version = 13
/*
This migration modifies the network config from ambiguous 'testnet' to explicit 'ropsten'
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
if (newState.config.provider.type === 'testnet') {
newState.config.provider.type = 'ropsten'
}
return newState
}

@ -0,0 +1,34 @@
const version = 14
/*
This migration removes provider from config and moves it too NetworkController.
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
newState.NetworkController = {}
newState.NetworkController.provider = newState.config.provider
delete newState.config.provider
return newState
}

@ -15,15 +15,15 @@ const KeyringController = require('../../app/scripts/lib/keyring-controller')
const password = 'obviously not correct' const password = 'obviously not correct'
module.exports = { module.exports = {
version, version,
migrate: function (versionedData) { migrate: function (versionedData) {
versionedData.meta.version = version versionedData.meta.version = version
let store = new ObservableStore(versionedData.data) const store = new ObservableStore(versionedData.data)
let configManager = new ConfigManager({ store }) const configManager = new ConfigManager({ store })
let idStoreMigrator = new IdentityStoreMigrator({ configManager }) const idStoreMigrator = new IdentityStoreMigrator({ configManager })
let keyringController = new KeyringController({ const keyringController = new KeyringController({
configManager: configManager, configManager: configManager,
}) })
@ -46,6 +46,5 @@ module.exports = {
return Promise.resolve(versionedData) return Promise.resolve(versionedData)
}) })
}) })
}, },
} }

@ -23,4 +23,6 @@ module.exports = [
require('./010'), require('./010'),
require('./011'), require('./011'),
require('./012'), require('./012'),
require('./013'),
require('./014'),
] ]

@ -1,7 +1,7 @@
const EventEmitter = require('events').EventEmitter const EventEmitter = require('events').EventEmitter
const async = require('async') const async = require('async')
const Dnode = require('dnode') const Dnode = require('dnode')
const Web3 = require('web3') const EthQuery = require('eth-query')
const launchMetamaskUi = require('../../ui') const launchMetamaskUi = require('../../ui')
const StreamProvider = require('web3-stream-provider') const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
@ -16,7 +16,6 @@ function initializePopup ({ container, connectionStream }, cb) {
(cb) => connectToAccountManager(connectionStream, cb), (cb) => connectToAccountManager(connectionStream, cb),
(accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb), (accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb),
], cb) ], cb)
} }
function connectToAccountManager (connectionStream, cb) { function connectToAccountManager (connectionStream, cb) {
@ -33,7 +32,8 @@ function setupWeb3Connection (connectionStream) {
providerStream.pipe(connectionStream).pipe(providerStream) providerStream.pipe(connectionStream).pipe(providerStream)
connectionStream.on('error', console.error.bind(console)) connectionStream.on('error', console.error.bind(console))
providerStream.on('error', console.error.bind(console)) providerStream.on('error', console.error.bind(console))
global.web3 = new Web3(providerStream) global.ethereumProvider = providerStream
global.ethQuery = new EthQuery(providerStream)
} }
function setupControllerConnection (connectionStream, cb) { function setupControllerConnection (connectionStream, cb) {

@ -41,7 +41,7 @@ function closePopupIfOpen (windowType) {
} }
} }
function displayCriticalError(err) { function displayCriticalError (err) {
container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>' container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>'
container.style.height = '80px' container.style.height = '80px'
log.error(err.stack) log.error(err.stack)

@ -1,6 +1,6 @@
machine: machine:
node: node:
version: 6.0.0 version: 7.6.0
dependencies: dependencies:
pre: pre:
- "npm i -g testem" - "npm i -g testem"

@ -7,6 +7,6 @@ var changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md')).toSt
var log = changelog.split(version)[1].split('##')[0].trim() var log = changelog.split(version)[1].split('##')[0].trim()
let msg = `*MetaMask ${version}* now published to the Chrome Store! It should auto-update over the next hour!\n${log}` let msg = `*MetaMask ${version}* now published to the Chrome Store! It should auto-update soon!\n${log}`
console.log(msg) console.log(msg)

@ -0,0 +1,14 @@
## Add Custom Build to Chrome
Open `Settings` > `Extensions`.
Check "Developer mode".
At the top, click `Load Unpacked Extension`.
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.

@ -0,0 +1,14 @@
# Add Custom Build to 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`.

@ -0,0 +1,25 @@
## Adding Custom Networks
To add another network to our dropdown menu, make sure the following files are adjusted properly:
```
app/scripts/config.js
app/scripts/lib/buy-eth-url.js
app/scripts/lib/config-manager.js
ui/app/app.js
ui/app/components/buy-button-subview.js
ui/app/components/drop-menu-item.js
ui/app/components/network.js
ui/app/components/transaction-list-item.js
ui/app/config.js
ui/app/css/lib.css
ui/lib/account-link.js
ui/lib/explorer-link.js
```
You will need:
+ The network ID
+ An RPC Endpoint url
+ An explorer link
+ CSS for the display icon

@ -0,0 +1,10 @@
### Developing on Dependencies
To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies:
1. Clone the dependency locally.
2. `npm install` in its folder.
3. Run `npm link` in its folder.
4. Run `npm link $DEP_NAME` in this project folder.
5. Next time you `npm start` it will watch the dependency for changes as well!

@ -0,0 +1,35 @@
### Generate Development Visualization
This will generate a video of the repo commit history.
Install preqs:
```
brew install gource
brew install ffmpeg
```
From the repo dir, pipe `gource` into `ffmpeg`:
```
gource \
--seconds-per-day .1 \
--user-scale 1.5 \
--default-user-image "./images/icon-512.png" \
--viewport 1280x720 \
--auto-skip-seconds .1 \
--multi-sampling \
--stop-at-end \
--highlight-users \
--hide mouse,progress \
--file-idle-time 0 \
--max-files 0 \
--background-colour 000000 \
--font-size 18 \
--date-format "%b %d, %Y" \
--highlight-dirs \
--user-friction 0.1 \
--title "MetaMask Development History" \
--output-ppm-stream - \
--output-framerate 30 \
| ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4
```

@ -0,0 +1,15 @@
## Generating Notices
To add a notice:
```
npm run generateNotice
```
Enter the body of your notice into the text editor that pops up, without including the body. Be sure to save the file before closing the window!
Afterwards, enter the title of the notice in the command line and press enter. Afterwards, add and commit the new changes made.
To delete a notice:
```
npm run deleteNotice
```
A list of active notices will pop up. Enter the corresponding id in the command line prompt and add and commit the new changes afterwards.

@ -0,0 +1,19 @@
# Publishing Guide
When publishing a new version of MetaMask, we follow this procedure:
## Incrementing Version & Changelog
You must be authorized already on the MetaMask plugin.
1. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`.
2. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2).
## Publishing
1. `npm run dist` to generate the latest build.
2. Publish to chrome store.
3. Publish to firefox addon marketplace.
4. Post on Github releases page.
5. `npm run announce`, post that announcement in our public places.

@ -0,0 +1,6 @@
# Running UI Dev Mode
You can run `npm run ui`, and your browser should open a live-reloading demo version of the plugin UI.
Some actions will crash the app, so this is only for tuning aesthetics, but it allows live-reloading styles, which is a much faster feedback loop than reloading the full extension.

@ -0,0 +1,8 @@
### Developing on UI with Mocked Background Process
You can run `npm run mock` and your browser should open a live-reloading demo version of the plugin UI, just like the `npm run ui`, except that it tries to actually perform all normal operations.
It does not yet connect to a real blockchain (this could be a good test feature later, connecting to a test blockchain), so only local operations work.
You can reset the mock ui at any time with the `Reset` button at the top of the screen.

@ -52,6 +52,15 @@ gulp.task('copy:images', copyTask({
'./dist/opera/images', './dist/opera/images',
], ],
})) }))
gulp.task('copy:contractImages', copyTask({
source: './node_modules/ethereum-contract-icons/images/',
destinations: [
'./dist/firefox/images/contract',
'./dist/chrome/images/contract',
'./dist/edge/images/contract',
'./dist/opera/images/contract',
],
}))
gulp.task('copy:fonts', copyTask({ gulp.task('copy:fonts', copyTask({
source: './app/fonts/', source: './app/fonts/',
destinations: [ destinations: [
@ -127,6 +136,7 @@ const staticFiles = [
] ]
var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`) var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`)
copyStrings.push('copy:contractImages')
if (!disableLiveReload) { if (!disableLiveReload) {
copyStrings.push('copy:reload') copyStrings.push('copy:reload')
@ -182,7 +192,7 @@ gulp.task('build:js', gulp.parallel(...jsBuildStrings))
// disc bundle analyzer tasks // disc bundle analyzer tasks
jsFiles.forEach((jsFile) => { jsFiles.forEach((jsFile) => {
gulp.task(`disc:${jsFile}`, bundleTask({ label: jsFile, filename: `${jsFile}.js` })) gulp.task(`disc:${jsFile}`, discTask({ label: jsFile, filename: `${jsFile}.js` }))
}) })
gulp.task('disc', gulp.parallel(jsFiles.map(jsFile => `disc:${jsFile}`))) gulp.task('disc', gulp.parallel(jsFiles.map(jsFile => `disc:${jsFile}`)))
@ -296,8 +306,6 @@ function bundleTask(opts) {
return ( return (
bundler.bundle() bundler.bundle()
// log errors if they happen
.on('error', gutil.log.bind(gutil, 'Browserify Error'))
// convert bundle stream to gulp vinyl stream // convert bundle stream to gulp vinyl stream
.pipe(source(opts.filename)) .pipe(source(opts.filename))
// inject variables into bundle // inject variables into bundle

@ -1,20 +1,33 @@
start the dual servers (dapp + mascara) start the dual servers (dapp + mascara)
``` ```
node server.js npm run mascara
``` ```
## First time use: ### First time use:
- navigate to: http://localhost:9001/popup/popup.html - navigate to: http://localhost:9001
- Create an Account - Create an Account
- go back to http://localhost:9002/ - go back to http://localhost:9002
- open devTools - open devTools
- click Sync Tx - click Sync Tx
### Todos ### Tests:
- [ ] Figure out user flows and UI redesign ```
- [ ] Figure out FireFox npm run testMascara
Standing problems: ```
- [ ] IndexDb
Test will run in browser, you will have to have these browsers installed:
- Chrome
- Firefox
- Opera
### Deploy:
Will build and deploy mascara via docker
```
docker-compose build && docker-compose stop && docker-compose up -d && docker-compose logs --tail 200 -f
```

@ -1,4 +1,5 @@
global.window = global global.window = global
const self = global
const pipe = require('pump') const pipe = require('pump')
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js') const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
@ -6,7 +7,7 @@ const connectionListener = new SwGlobalListener(self)
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
const PortStream = require('../../app/scripts/lib/port-stream.js') const PortStream = require('../../app/scripts/lib/port-stream.js')
const DbController = require('./lib/index-db-controller') const DbController = require('idb-global')
const SwPlatform = require('../../app/scripts/platforms/sw') const SwPlatform = require('../../app/scripts/platforms/sw')
const MetamaskController = require('../../app/scripts/metamask-controller') const MetamaskController = require('../../app/scripts/metamask-controller')
@ -21,6 +22,7 @@ const STORAGE_KEY = 'metamask-config'
// const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' // const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const METAMASK_DEBUG = true const METAMASK_DEBUG = true
let popupIsOpen = false let popupIsOpen = false
let connectedClientCount = 0
const log = require('loglevel') const log = require('loglevel')
global.log = log global.log = log
@ -40,7 +42,6 @@ console.log('inside:open')
let diskStore let diskStore
const dbController = new DbController({ const dbController = new DbController({
key: STORAGE_KEY, key: STORAGE_KEY,
version: 2,
}) })
loadStateFromPersistence() loadStateFromPersistence()
.then((initState) => setupController(initState)) .then((initState) => setupController(initState))
@ -107,6 +108,7 @@ function setupController (initState, client) {
connectionListener.on('remote', (portStream, messageEvent) => { connectionListener.on('remote', (portStream, messageEvent) => {
console.log('REMOTE CONECTION FOUND***********') console.log('REMOTE CONECTION FOUND***********')
connectedClientCount += 1
connectRemote(portStream, messageEvent.data.context) connectRemote(portStream, messageEvent.data.context)
}) })
@ -142,4 +144,12 @@ function setupController (initState, client) {
return Promise.resolve() return Promise.resolve()
} }
function sendMessageToAllClients (message) {
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage(message)
})
})
}
function noop () {} function noop () {}

@ -1,88 +0,0 @@
const EventEmitter = require('events')
module.exports = class IndexDbController extends EventEmitter {
constructor (opts) {
super()
this.migrations = opts.migrations
this.key = opts.key
this.dbObject = global.indexedDB
this.IDBTransaction = global.IDBTransaction || global.webkitIDBTransaction || global.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers
this.IDBKeyRange = global.IDBKeyRange || global.webkitIDBKeyRange || global.msIDBKeyRange;
this.version = opts.version
this.logging = opts.logging
this.initialState = opts.initialState
if (this.logging) this.on('log', logger)
}
// Opens the database connection and returns a promise
open (version = this.version) {
return new Promise((resolve, reject) => {
const dbOpenRequest = this.dbObject.open(this.key, version)
dbOpenRequest.onerror = (event) => {
return reject(event)
}
dbOpenRequest.onsuccess = (event) => {
this.db = dbOpenRequest.result
this.emit('success')
resolve(this.db)
}
dbOpenRequest.onupgradeneeded = (event) => {
this.db = event.target.result
this.db.createObjectStore('dataStore')
}
})
.then((openRequest) => {
return this.get('dataStore')
})
.then((data) => {
if (!data) {
return this._add('dataStore', this.initialState)
.then(() => this.get('dataStore'))
.then((versionedData) => Promise.resolve(versionedData))
}
return Promise.resolve(data)
})
}
requestObjectStore (key, type = 'readonly') {
return new Promise((resolve, reject) => {
const dbReadWrite = this.db.transaction(key, type)
const dataStore = dbReadWrite.objectStore(key)
resolve(dataStore)
})
}
get (key = 'dataStore') {
return this.requestObjectStore(key)
.then((dataObject)=> {
return new Promise((resolve, reject) => {
const getRequest = dataObject.get(key)
getRequest.onsuccess = (event) => resolve(event.currentTarget.result)
getRequest.onerror = (event) => reject(event)
})
})
}
put (state) {
return this.requestObjectStore('dataStore', 'readwrite')
.then((dataObject)=> {
const putRequest = dataObject.put(state, 'dataStore')
putRequest.onsuccess = (event) => Promise.resolve(event.currentTarget.result)
putRequest.onerror = (event) => Promise.reject(event)
})
}
_add (key, objStore, cb = logger) {
return this.requestObjectStore(key, 'readwrite')
.then((dataObject)=> {
const addRequest = dataObject.add(objStore, key)
addRequest.onsuccess = (event) => Promise.resolve(event.currentTarget.result)
addRequest.onerror = (event) => Promise.reject(event)
})
}
}
function logger (err, ress) {
err ? console.error(`Logger says: ${err}`) : console.dir(`Logger says: ${ress}`)
}

@ -1,6 +1,6 @@
const Web3 = require('web3') const Web3 = require('web3')
const setupProvider = require('./lib/setup-provider.js') const setupProvider = require('./lib/setup-provider.js')
const setupDappAutoReload = require('../../app/scripts/lib/auto-reload.js')
const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001' const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001'
console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN) console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN)
@ -14,8 +14,7 @@ const provider = setupProvider({
instrumentForUserInteractionTriggers(provider) instrumentForUserInteractionTriggers(provider)
const web3 = new Web3(provider) const web3 = new Web3(provider)
global.web3 = web3 setupDappAutoReload(web3, provider.publicConfigStore)
// //
// ui stuff // ui stuff
// //

@ -20,6 +20,7 @@ background.on('ready', (_) => {
pageStream.pipe(swStream).pipe(pageStream) pageStream.pipe(swStream).pipe(pageStream)
}) })
background.on('updatefound', () => window.location.reload())
background.on('error', console.error) background.on('error', console.error)
background.startWorker() background.startWorker()

@ -24,10 +24,10 @@ const background = new SWcontroller({
fileName: '/background.js', fileName: '/background.js',
letBeIdle: false, letBeIdle: false,
intervalDelay, intervalDelay,
wakeUpInterval: 30000 wakeUpInterval: 20000
}) })
// Setup listener for when the service worker is read // Setup listener for when the service worker is read
background.on('ready', (readSw) => { const connectApp = function (readSw) {
let connectionStream = SwStream({ let connectionStream = SwStream({
serviceWorker: background.controller, serviceWorker: background.controller,
context: name, context: name,
@ -39,6 +39,18 @@ background.on('ready', (readSw) => {
if (state.appState.shouldClose) window.close() if (state.appState.shouldClose) window.close()
}) })
}) })
}
background.on('ready', (sw) => {
background.removeListener('updatefound', connectApp)
connectApp(sw)
}) })
background.on('updatefound', () => window.location.reload())
background.startWorker() background.startWorker()
.then(() => {
setTimeout(() => {
const appContent = document.getElementById(`app-content`)
if (!appContent.children.length) window.location.reload()
}, 2000)
})
console.log('hello from MetaMascara ui!') console.log('hello from MetaMascara ui!')

@ -0,0 +1,7 @@
function wait(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve()
}, time * 3 || 1500)
})
}

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>QUnit Example</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.0.0.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-2.0.0.js"></script>
<script src="./jquery-3.1.0.min.js"></script>
<script src="./helpers.js"></script>
<script src="./test-bundle.js"></script>
<script src="/testem.js"></script>
<div id="app-content"></div>
<script src="./bundle.js"></script>
</body>
</html>

@ -0,0 +1,22 @@
var fs = require('fs')
var path = require('path')
var browserify = require('browserify');
var tests = fs.readdirSync(path.join(__dirname, 'lib'))
var bundlePath = path.join(__dirname, 'test-bundle.js')
var b = browserify();
// Remove old bundle
try {
fs.unlinkSync(bundlePath)
} catch (e) {
console.error(e)
}
var writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function(fileName) {
b.add(path.join(__dirname, 'lib', fileName))
})
b.bundle().pipe(writeStream);

File diff suppressed because one or more lines are too long

@ -0,0 +1,119 @@
const PASSWORD = 'password123'
QUnit.module('first time usage')
QUnit.test('render init screen', function (assert) {
var done = assert.async()
let app
wait(1000).then(function() {
app = $('#app-content').contents()
const recurseNotices = function () {
let button = app.find('button')
if (button.html() === 'Accept') {
let termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight
return wait().then(() => {
button.click()
return wait()
}).then(() => {
return recurseNotices()
})
} else {
return wait()
}
}
return recurseNotices()
}).then(function() {
// Scroll through terms
var title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
// enter password
var pwBox = app.find('#password-box')[0]
var confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
return wait()
}).then(function() {
// create vault
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1500)
}).then(function() {
var created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
// Agree button
var button = app.find('button')[0]
assert.ok(button, 'button present')
button.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
var sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
return wait()
}).then(function() {
var sandwich = app.find('.menu-droppo')[0]
var children = sandwich.children
var lock = children[children.length - 2]
assert.ok(lock, 'Lock menu item found')
lock.click()
return wait(1000)
}).then(function() {
var pwBox = app.find('#password-box')[0]
pwBox.value = PASSWORD
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded again.')
return wait()
}).then(function (){
var qrButton = app.find('.fa.fa-qrcode')[0]
qrButton.click()
return wait(1000)
}).then(function (){
var qrHeader = app.find('.qr-header')[0]
var qrContainer = app.find('#qr-container')[0]
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
assert.ok(qrContainer, 'QR Container found')
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
networkMenu.click()
return wait()
}).then(function (){
var networkMenu = app.find('.network-indicator')[0]
var children = networkMenu.children
children.length[3]
assert.ok(children, 'All network options present')
done()
})
})

@ -0,0 +1,13 @@
launch_in_dev:
- Chrome
- Firefox
- Opera
launch_in_ci:
- Chrome
- Firefox
- Opera
framework:
- qunit
before_tests: "npm run mascaraCi"
after_tests: "rm ./background.js ./test-bundle.js ./bundle.js"
test_page: "./index.html"

@ -0,0 +1,40 @@
const EventEmitter = require('events')
const IDB = require('idb-global')
const KEY = 'metamask-test-config'
module.exports = class Helper extends EventEmitter {
constructor () {
super()
}
tryToCleanContext () {
this.unregister()
.then(() => this.clearDb())
.then(() => super.emit('complete'))
.catch((err) => super.emit('complete'))
}
unregister () {
return global.navigator.serviceWorker.getRegistration()
.then((registration) => {
if (registration) return registration.unregister()
.then((b) => b ? Promise.resolve() : Promise.reject())
else return Promise.resolve()
})
}
clearDb () {
return new Promise ((resolve, reject) => {
const deleteRequest = global.indexDB.deleteDatabase(KEY)
deleteRequest.addEventListener('success', resolve)
deleteRequest.addEventListener('error', reject)
})
}
mockState (state) {
const db = new IDB({
version: 2,
key: KEY,
initialState: state
})
return db.open()
}
}

@ -0,0 +1,5 @@
const Helper = require('./util/mascara-test-helper.js')
window.addEventListener('load', () => {
require('../src/ui.js')
})

@ -2,7 +2,8 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>MetaMask Plugin</title> <title>MetaMascara Alpha</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>
<div id="app-content"></div> <div id="app-content"></div>

@ -1 +0,0 @@
MetaMask now lists a new network on our dropdown list: Kovan, a [Proof of Authority](https://github.com/paritytech/parity/wiki/Proof-of-Authority-Chains) testchain managed by several blockchain organizations such as Digix, Etherscan, and Parity. It is designed to be a more stable and reliable testnet alternative to Ropsten and was created in response to recent attacks that slowed down the Ropsten network. You can read more about Kovan [here](https://medium.com/@Digix/announcing-kovan-a-stable-ethereum-public-testnet-10ac7cb6c85f#.6o8sz8cct) and [here](https://medium.com/@Digix/letter-from-the-ceo-some-context-regarding-kovan-7b5121adb901#.kfv7zhw83). As with Ropsten, the default remote node to connect to Kovan is managed by Infura.

@ -0,0 +1,8 @@
MetaMask is beta software.
When you log in to MetaMask, your current account is visible to every new site you visit.
For your privacy, for now, please sign out of MetaMask when you're done using a site.
Also, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.

File diff suppressed because one or more lines are too long

@ -7,7 +7,7 @@
"start": "npm run dev", "start": "npm run dev",
"dev": "gulp dev --debug", "dev": "gulp dev --debug",
"disc": "gulp disc --debug", "disc": "gulp disc --debug",
"dist": "gulp dist --disableLiveReload", "dist": "npm install && gulp dist --disableLiveReload",
"test": "npm run lint && npm run test-unit && npm run test-integration", "test": "npm run lint && npm run test-unit && npm run test-integration",
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
@ -21,7 +21,12 @@
"testem": "npm run buildMock && testem", "testem": "npm run buildMock && testem",
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"generateNotice": "node notices/notice-generator.js", "generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js" "deleteNotice": "node notices/notice-delete.js",
"mascara": "node ./mascara/example/server",
"buildMascaraCi": "browserify mascara/test/window-load.js -o mascara/test/bundle.js",
"buildMascaraSWCi": "browserify mascara/src/background.js -o mascara/test/background.js",
"mascaraCi": "npm run buildMascaraCi && npm run buildMascaraSWCi && node mascara/test/index.js",
"testMascara": "cd mascara/test && npm run mascaraCi && testem ci -P 3"
}, },
"browserify": { "browserify": {
"transform": [ "transform": [
@ -29,7 +34,8 @@
"babelify", "babelify",
{ {
"presets": [ "presets": [
"es2015" "es2015",
"stage-0"
] ]
} }
], ],
@ -39,36 +45,39 @@
}, },
"dependencies": { "dependencies": {
"async": "^1.5.2", "async": "^1.5.2",
"async-q": "^0.3.1", "babel-runtime": "^6.23.0",
"bip39": "^2.2.0", "bip39": "^2.2.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"browser-passworder": "^2.0.3", "browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4", "browserify-derequire": "^0.9.4",
"client-sw-ready-event": "^3.0.1", "client-sw-ready-event": "^3.3.0",
"clone": "^1.0.2", "clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0", "copy-to-clipboard": "^2.0.0",
"debounce": "^1.0.0", "debounce": "^1.0.0",
"deep-extend": "^0.4.1", "deep-extend": "^0.4.1",
"denodeify": "^1.2.1", "denodeify": "^1.2.1",
"detect-node": "^2.0.3",
"disc": "^1.3.2", "disc": "^1.3.2",
"dnode": "^1.2.2", "dnode": "^1.2.2",
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
"ensnare": "^1.0.0", "ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-contract-metadata": "^1.0.0",
"eth-hd-keyring": "^1.1.1", "eth-hd-keyring": "^1.1.1",
"eth-query": "^1.0.3", "eth-query": "^2.1.1",
"eth-sig-util": "^1.1.1", "eth-sig-util": "^1.1.1",
"eth-simple-keyring": "^1.1.1", "eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.0.4", "eth-token-tracker": "^1.0.4",
"ethereumjs-tx": "^1.2.5", "ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
"ethjs-ens": "^1.0.2", "ethjs-ens": "^2.0.0",
"express": "^4.14.0", "express": "^4.14.0",
"extension-link-enabler": "^1.0.0", "extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0", "extensionizer": "^1.0.0",
"gulp-eslint": "^2.0.0", "gulp-eslint": "^2.0.0",
"hat": "0.0.3", "hat": "0.0.3",
"idb-global": "^1.0.0",
"identicon.js": "^1.2.1", "identicon.js": "^1.2.1",
"iframe": "^1.0.0", "iframe": "^1.0.0",
"iframe-stream": "^1.0.2", "iframe-stream": "^1.0.2",
@ -80,6 +89,7 @@
"mississippi": "^1.2.0", "mississippi": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"multiplex": "^6.7.0", "multiplex": "^6.7.0",
"number-to-bn": "^1.7.0",
"obs-store": "^2.3.1", "obs-store": "^2.3.1",
"once": "^1.3.3", "once": "^1.3.3",
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
@ -114,7 +124,7 @@
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "0.18.2", "web3": "0.18.2",
"web3-provider-engine": "^11.0.2", "web3-provider-engine": "^12.2.4",
"web3-stream-provider": "^2.0.6", "web3-stream-provider": "^2.0.6",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
@ -135,6 +145,9 @@
"deep-freeze-strict": "^1.1.1", "deep-freeze-strict": "^1.1.1",
"del": "^2.2.0", "del": "^2.2.0",
"envify": "^4.0.0", "envify": "^4.0.0",
"enzyme": "^2.8.2",
"eslint-plugin-chai": "0.0.1",
"eslint-plugin-mocha": "^4.9.0",
"fs-promise": "^1.0.0", "fs-promise": "^1.0.0",
"gulp": "github:gulpjs/gulp#4.0", "gulp": "github:gulpjs/gulp#4.0",
"gulp-if": "^2.0.1", "gulp-if": "^2.0.1",
@ -159,6 +172,10 @@
"prompt": "^1.0.0", "prompt": "^1.0.0",
"qs": "^6.2.0", "qs": "^6.2.0",
"qunit": "^0.9.1", "qunit": "^0.9.1",
"react-addons-test-utils": "^15.5.1",
"react-dom": "^15.5.4",
"react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0",
"sinon": "^1.17.3", "sinon": "^1.17.3",
"tape": "^4.5.1", "tape": "^4.5.1",
"testem": "^1.10.3", "testem": "^1.10.3",

@ -20,14 +20,12 @@ window.localStorage = {}
if (!window.crypto) window.crypto = {} if (!window.crypto) window.crypto = {}
if (!window.crypto.getRandomValues) window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues') if (!window.crypto.getRandomValues) window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues')
function enableFailureOnUnhandledPromiseRejection () {
function enableFailureOnUnhandledPromiseRejection() {
// overwrite node's promise with the stricter Bluebird promise // overwrite node's promise with the stricter Bluebird promise
global.Promise = require('bluebird') global.Promise = require('bluebird')
// modified from https://github.com/mochajs/mocha/issues/1926#issuecomment-180842722 // modified from https://github.com/mochajs/mocha/issues/1926#issuecomment-180842722
// rethrow unhandledRejections // rethrow unhandledRejections
if (typeof process !== 'undefined') { if (typeof process !== 'undefined') {
process.on('unhandledRejection', function (reason) { process.on('unhandledRejection', function (reason) {
@ -51,4 +49,4 @@ function enableFailureOnUnhandledPromiseRejection() {
typeof (console.error || console.log) === 'function') { typeof (console.error || console.log) === 'function') {
(console.error || console.log)('Unhandled rejections will be ignored!') (console.error || console.log)('Unhandled rejections will be ignored!')
} }
} }

@ -1,6 +1,6 @@
function wait(time) { function wait(time) {
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
setTimeout(function() { setTimeout(function () {
resolve() resolve()
}, time * 3 || 1500) }, time * 3 || 1500)
}) })

@ -1,10 +1,10 @@
var fs = require('fs') var fs = require('fs')
var path = require('path') var path = require('path')
var browserify = require('browserify'); var browserify = require('browserify')
var tests = fs.readdirSync(path.join(__dirname, 'lib')) var tests = fs.readdirSync(path.join(__dirname, 'lib'))
var bundlePath = path.join(__dirname, 'bundle.js') var bundlePath = path.join(__dirname, 'bundle.js')
var b = browserify(); var b = browserify()
// Remove old bundle // Remove old bundle
try { try {
@ -13,9 +13,9 @@ try {
var writeStream = fs.createWriteStream(bundlePath) var writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function(fileName) { tests.forEach(function (fileName) {
b.add(path.join(__dirname, 'lib', fileName)) b.add(path.join(__dirname, 'lib', fileName))
}) })
b.bundle().pipe(writeStream); b.bundle().pipe(writeStream)

@ -11,7 +11,7 @@ QUnit.test('render init screen', function (assert) {
const recurseNotices = function () { const recurseNotices = function () {
let button = app.find('button') let button = app.find('button')
if (button.html() === 'Continue') { if (button.html() === 'Accept') {
let termsPage = app.find('.markdown')[0] let termsPage = app.find('.markdown')[0]
termsPage.scrollTop = termsPage.scrollHeight termsPage.scrollTop = termsPage.scrollHeight
return wait().then(() => { return wait().then(() => {

@ -1 +1,14 @@
{"version":0,"data":{"wallet":"{\"encSeed\":{\"encStr\":\"rT1C1jjkFRfmrwefscFcwZohl4f+HfIFlBZ9AM4ZD8atJmfKDIQCVK11NYDKYv8ZMIY03f3t8MuoZvfzBL8IJsWnZUhpzVTNNiARQJD2WpGA19eNBzgZm4vd0GwkIUruUDeJXu0iv2j9wU8hOQUqPbOePPy2Am5ro97iuvMAroRTnEKD60qFVg==\",\"nonce\":\"YUY2mwNq2v3FV0Fi94QnSiKFOLYfDR95\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"Iyi7ft4JQ9UtwrSXRT6ZIHPtZqJhe99rh0uWhNc6QLan6GanY2ZQeU0tt76CBealEWJyrJReSxGQdqDmSDYjpjH3m4JO5l0DfPLPseCqzXV/W+dzM0ubJ8lztLwpwi0L+vULNMqCx4dQtoNbNBq1QZUnjtpm6O8mWpScspboww==\",\"nonce\":\"Z7RqtjNjC6FrLUj5wVW1+HkjOW6Hib6K\"},\"hdIndex\":3,\"encPrivKeys\":{\"edb81c10122f34040cc4bef719a272fbbb1cf897\":{\"key\":\"8ab81tKBd4+CLAbzvS7SBFRTd6VWXBs86uBE43lgcmBu2U7UB22xdH64Q2hUf9eB\",\"nonce\":\"aGUEqI033FY39zKjWmZSI6PQrCLvkiRP\"},\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\":{\"key\":\"+i3wmf4b+B898QtlOBfL0Ixirjg59/LLPX61vQ2L0xRPjXzNog0O4Wn15RemM5mY\",\"nonce\":\"imKrlkuoC5uuFkzJBbuDBluGCPJXNTKm\"},\"2340695474656e3124b8eba1172fbfb00eeac8f8\":{\"key\":\"pi+H9D8LYKsdCQKrfaJtsGFjE+X9s74xN675tsoIKrbPXhtpxMLOIQVtSqYveF62\",\"nonce\":\"49g80wDTovHwbguVVYf2FsYbp7Db5OAR\"}},\"addresses\":[\"edb81c10122f34040cc4bef719a272fbbb1cf897\",\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\",\"2340695474656e3124b8eba1172fbfb00eeac8f8\"]}},\"version\":2}","config":{"provider":{"type":"etherscan"}}},"meta":{"version":0}} {
"version": 0,
"data": {
"wallet": "{\"encSeed\":{\"encStr\":\"rT1C1jjkFRfmrwefscFcwZohl4f+HfIFlBZ9AM4ZD8atJmfKDIQCVK11NYDKYv8ZMIY03f3t8MuoZvfzBL8IJsWnZUhpzVTNNiARQJD2WpGA19eNBzgZm4vd0GwkIUruUDeJXu0iv2j9wU8hOQUqPbOePPy2Am5ro97iuvMAroRTnEKD60qFVg==\",\"nonce\":\"YUY2mwNq2v3FV0Fi94QnSiKFOLYfDR95\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"Iyi7ft4JQ9UtwrSXRT6ZIHPtZqJhe99rh0uWhNc6QLan6GanY2ZQeU0tt76CBealEWJyrJReSxGQdqDmSDYjpjH3m4JO5l0DfPLPseCqzXV/W+dzM0ubJ8lztLwpwi0L+vULNMqCx4dQtoNbNBq1QZUnjtpm6O8mWpScspboww==\",\"nonce\":\"Z7RqtjNjC6FrLUj5wVW1+HkjOW6Hib6K\"},\"hdIndex\":3,\"encPrivKeys\":{\"edb81c10122f34040cc4bef719a272fbbb1cf897\":{\"key\":\"8ab81tKBd4+CLAbzvS7SBFRTd6VWXBs86uBE43lgcmBu2U7UB22xdH64Q2hUf9eB\",\"nonce\":\"aGUEqI033FY39zKjWmZSI6PQrCLvkiRP\"},\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\":{\"key\":\"+i3wmf4b+B898QtlOBfL0Ixirjg59/LLPX61vQ2L0xRPjXzNog0O4Wn15RemM5mY\",\"nonce\":\"imKrlkuoC5uuFkzJBbuDBluGCPJXNTKm\"},\"2340695474656e3124b8eba1172fbfb00eeac8f8\":{\"key\":\"pi+H9D8LYKsdCQKrfaJtsGFjE+X9s74xN675tsoIKrbPXhtpxMLOIQVtSqYveF62\",\"nonce\":\"49g80wDTovHwbguVVYf2FsYbp7Db5OAR\"}},\"addresses\":[\"edb81c10122f34040cc4bef719a272fbbb1cf897\",\"8bd7d5c000cf05284e98356370dc5ccaa3dbfc38\",\"2340695474656e3124b8eba1172fbfb00eeac8f8\"]}},\"version\":2}",
"config": {
"provider": {
"type": "etherscan"
}
}
},
"meta": {
"version": 0
}
}

@ -2,9 +2,8 @@ const ObservableStore = require('obs-store')
const clone = require('clone') const clone = require('clone')
const ConfigManager = require('../../app/scripts/lib/config-manager') const ConfigManager = require('../../app/scripts/lib/config-manager')
const firstTimeState = require('../../app/scripts/first-time-state') const firstTimeState = require('../../app/scripts/first-time-state')
const STORAGE_KEY = 'metamask-config'
module.exports = function() { module.exports = function () {
let store = new ObservableStore(clone(firstTimeState)) const store = new ObservableStore(clone(firstTimeState))
return new ConfigManager({ store }) return new ConfigManager({ store })
} }

@ -4,28 +4,28 @@ let cacheVal
module.exports = { module.exports = {
encrypt(password, dataObj) { encrypt (password, dataObj) {
cacheVal = dataObj cacheVal = dataObj
return Promise.resolve(mockHex) return Promise.resolve(mockHex)
}, },
decrypt(password, text) { decrypt (password, text) {
return Promise.resolve(cacheVal || {}) return Promise.resolve(cacheVal || {})
}, },
encryptWithKey(key, dataObj) { encryptWithKey (key, dataObj) {
return this.encrypt(key, dataObj) return this.encrypt(key, dataObj)
}, },
decryptWithKey(key, text) { decryptWithKey (key, text) {
return this.decrypt(key, text) return this.decrypt(key, text)
}, },
keyFromPassword(password) { keyFromPassword (password) {
return Promise.resolve(mockKey) return Promise.resolve(mockKey)
}, },
generateSalt() { generateSalt () {
return 'WHADDASALT!' return 'WHADDASALT!'
}, },

@ -6,32 +6,32 @@ const type = 'Simple Key Pair'
module.exports = class MockSimpleKeychain { module.exports = class MockSimpleKeychain {
static type() { return type } static type () { return type }
constructor(opts) { constructor (opts) {
this.type = type this.type = type
this.opts = opts || {} this.opts = opts || {}
this.wallets = [] this.wallets = []
} }
serialize() { serialize () {
return [ fakeWallet.privKey ] return [ fakeWallet.privKey ]
} }
deserialize(data) { deserialize (data) {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error('Simple keychain deserialize requires a privKey array.') throw new Error('Simple keychain deserialize requires a privKey array.')
} }
this.wallets = [ fakeWallet ] this.wallets = [ fakeWallet ]
} }
addAccounts(n = 1) { addAccounts (n = 1) {
for(var i = 0; i < n; i++) { for (var i = 0; i < n; i++) {
this.wallets.push(fakeWallet) this.wallets.push(fakeWallet)
} }
} }
getAccounts() { getAccounts () {
return this.wallets.map(w => w.address) return this.wallets.map(w => w.address)
} }

@ -0,0 +1,18 @@
const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk')
const createLogger = require('redux-logger')
const rootReducer = function () {}
module.exports = configureStore
const loggerMiddleware = createLogger()
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore)
function configureStore (initialState) {
return createStoreWithMiddleware(rootReducer, initialState)
}

@ -1,18 +1,16 @@
var assert = require('assert') var assert = require('assert')
var linkGen = require('../../ui/lib/account-link') var linkGen = require('../../ui/lib/account-link')
describe('account-link', function() { describe('account-link', function () {
it('adds ropsten prefix to ropsten test network', function () {
it('adds ropsten prefix to ropsten test network', function() {
var result = linkGen('account', '3') var result = linkGen('account', '3')
assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten included') assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten included')
assert.notEqual(result.indexOf('account'), -1, 'account included') assert.notEqual(result.indexOf('account'), -1, 'account included')
}) })
it('adds kovan prefix to kovan test network', function() { it('adds kovan prefix to kovan test network', function () {
var result = linkGen('account', '42') var result = linkGen('account', '42')
assert.notEqual(result.indexOf('kovan'), -1, 'kovan included') assert.notEqual(result.indexOf('kovan'), -1, 'kovan included')
assert.notEqual(result.indexOf('account'), -1, 'account included') assert.notEqual(result.indexOf('account'), -1, 'account included')
}) })
}) })

@ -1,36 +1,34 @@
var jsdom = require('mocha-jsdom') // var jsdom = require('mocha-jsdom')
var assert = require('assert') var assert = require('assert')
var freeze = require('deep-freeze-strict') var freeze = require('deep-freeze-strict')
var path = require('path') var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe ('config view actions', function() {
describe('config view actions', function () {
var initialState = { var initialState = {
metamask: { metamask: {
rpcTarget: 'foo', rpcTarget: 'foo',
frequentRpcList: [] frequentRpcList: [],
}, },
appState: { appState: {
currentView: { currentView: {
name: 'accounts', name: 'accounts',
} },
} },
} }
freeze(initialState) freeze(initialState)
describe('SHOW_CONFIG_PAGE', function() { describe('SHOW_CONFIG_PAGE', function () {
it('should set appState.currentView.name to config', function() { it('should set appState.currentView.name to config', function () {
var result = reducers(initialState, actions.showConfigPage()) var result = reducers(initialState, actions.showConfigPage())
assert.equal(result.appState.currentView.name, 'config') assert.equal(result.appState.currentView.name, 'config')
}) })
}) })
describe('SET_RPC_TARGET', function() { describe('SET_RPC_TARGET', function () {
it('sets the state.metamask.rpcTarget property of the state to the action.value', function () {
it('sets the state.metamask.rpcTarget property of the state to the action.value', function() {
const action = { const action = {
type: actions.SET_RPC_TARGET, type: actions.SET_RPC_TARGET,
value: 'foo', value: 'foo',
@ -41,5 +39,4 @@ describe ('config view actions', function() {
assert.equal(result.metamask.provider.rpcTarget, 'foo') assert.equal(result.metamask.provider.rpcTarget, 'foo')
}) })
}) })
}) })

@ -1,22 +1,21 @@
var jsdom = require('mocha-jsdom') // var jsdom = require('mocha-jsdom')
var assert = require('assert') var assert = require('assert')
var freeze = require('deep-freeze-strict') var freeze = require('deep-freeze-strict')
var path = require('path') var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('SAVE_ACCOUNT_LABEL', function() { describe('SAVE_ACCOUNT_LABEL', function () {
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () {
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function() {
var initialState = { var initialState = {
metamask: { metamask: {
identities: { identities: {
foo: { foo: {
name: 'bar' name: 'bar',
} },
}, },
} },
} }
freeze(initialState) freeze(initialState)
@ -24,13 +23,13 @@ describe('SAVE_ACCOUNT_LABEL', function() {
type: actions.SAVE_ACCOUNT_LABEL, type: actions.SAVE_ACCOUNT_LABEL,
value: { value: {
account: 'foo', account: 'foo',
label: 'baz' label: 'baz',
}, },
} }
freeze(action) freeze(action)
var resultingState = reducers(initialState, action) var resultingState = reducers(initialState, action)
assert.equal(resultingState.metamask.identities.foo.name, action.value.label) assert.equal(resultingState.metamask.identities.foo.name, action.value.label)
}); })
}); })

@ -1,18 +1,17 @@
var jsdom = require('mocha-jsdom') // var jsdom = require('mocha-jsdom')
var assert = require('assert') var assert = require('assert')
var freeze = require('deep-freeze-strict') var freeze = require('deep-freeze-strict')
var path = require('path') var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('SET_SELECTED_ACCOUNT', function() { describe('SET_SELECTED_ACCOUNT', function () {
it('sets the state.appState.activeAddress property of the state to the action.value', function () {
it('sets the state.appState.activeAddress property of the state to the action.value', function() {
var initialState = { var initialState = {
appState: { appState: {
activeAddress: 'foo', activeAddress: 'foo',
} },
} }
freeze(initialState) freeze(initialState)
@ -24,15 +23,15 @@ describe('SET_SELECTED_ACCOUNT', function() {
var resultingState = reducers(initialState, action) var resultingState = reducers(initialState, action)
assert.equal(resultingState.appState.activeAddress, action.value) assert.equal(resultingState.appState.activeAddress, action.value)
}); })
}); })
describe('SHOW_ACCOUNT_DETAIL', function() { describe('SHOW_ACCOUNT_DETAIL', function () {
it('updates metamask state', function() { it('updates metamask state', function () {
var initialState = { var initialState = {
metamask: { metamask: {
selectedAddress: 'foo' selectedAddress: 'foo',
} },
} }
freeze(initialState) freeze(initialState)

@ -1,29 +1,27 @@
var jsdom = require('mocha-jsdom') // var jsdom = require('mocha-jsdom')
var assert = require('assert') var assert = require('assert')
var freeze = require('deep-freeze-strict') var freeze = require('deep-freeze-strict')
var path = require('path') var path = require('path')
var sinon = require('sinon') var sinon = require('sinon')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('tx confirmation screen', function() { describe('tx confirmation screen', function () {
beforeEach(function () {
beforeEach(function() { this.sinon = sinon.sandbox.create()
this.sinon = sinon.sandbox.create(); })
});
afterEach(function(){ afterEach(function () {
this.sinon.restore(); this.sinon.restore()
}); })
var initialState, result var initialState, result
describe('when there is only one tx', function() { describe('when there is only one tx', function () {
var firstTxId = 1457634084250832 var firstTxId = 1457634084250832
beforeEach(function() { beforeEach(function () {
initialState = { initialState = {
appState: { appState: {
currentView: { currentView: {
@ -34,70 +32,66 @@ describe('tx confirmation screen', function() {
unapprovedTxs: { unapprovedTxs: {
'1457634084250832': { '1457634084250832': {
id: 1457634084250832, id: 1457634084250832,
status: "unconfirmed", status: 'unconfirmed',
time: 1457634084250, time: 1457634084250,
} },
}, },
} },
} }
freeze(initialState) freeze(initialState)
}) })
describe('cancelTx', function() { describe('cancelTx', function () {
before(function (done) {
before(function(done) {
actions._setBackgroundConnection({ actions._setBackgroundConnection({
approveTransaction(txId, cb) { cb('An error!') }, approveTransaction (txId, cb) { cb('An error!') },
cancelTransaction(txId) { /* noop */ }, cancelTransaction (txId) { /* noop */ },
clearSeedWordCache(cb) { cb() }, clearSeedWordCache (cb) { cb() },
}) })
let action = actions.cancelTx({value: firstTxId}) const action = actions.cancelTx({value: firstTxId})
result = reducers(initialState, action) result = reducers(initialState, action)
done() done()
}) })
it('should transition to the account detail view', function() { it('should transition to the account detail view', function () {
assert.equal(result.appState.currentView.name, 'accountDetail') assert.equal(result.appState.currentView.name, 'accountDetail')
}) })
it('should have no unconfirmed txs remaining', function() { it('should have no unconfirmed txs remaining', function () {
var count = getUnconfirmedTxCount(result) var count = getUnconfirmedTxCount(result)
assert.equal(count, 0) assert.equal(count, 0)
}) })
}) })
describe('sendTx', function() { describe('sendTx', function () {
var result var result
describe('when there is an error', function() { describe('when there is an error', function () {
before(function (done) {
before(function(done) {
alert = () => {/* noop */}
actions._setBackgroundConnection({ actions._setBackgroundConnection({
approveTransaction(txId, cb) { cb({message: 'An error!'}) }, approveTransaction (txId, cb) { cb({message: 'An error!'}) },
}) })
actions.sendTx({id: firstTxId})(function(action) { actions.sendTx({id: firstTxId})(function (action) {
result = reducers(initialState, action) result = reducers(initialState, action)
done() done()
}) })
}) })
it('should stay on the page', function() { it('should stay on the page', function () {
assert.equal(result.appState.currentView.name, 'confTx') assert.equal(result.appState.currentView.name, 'confTx')
}) })
it('should set errorMessage on the currentView', function() { it('should set errorMessage on the currentView', function () {
assert(result.appState.currentView.errorMessage) assert(result.appState.currentView.errorMessage)
}) })
}) })
describe('when there is success', function() { describe('when there is success', function () {
it('should complete tx and go home', function() { it('should complete tx and go home', function () {
actions._setBackgroundConnection({ actions._setBackgroundConnection({
approveTransaction(txId, cb) { cb() }, approveTransaction (txId, cb) { cb() },
}) })
var dispatchExpect = sinon.mock() var dispatchExpect = sinon.mock()
@ -108,10 +102,10 @@ describe('tx confirmation screen', function() {
}) })
}) })
describe('when there are two pending txs', function() { describe('when there are two pending txs', function () {
var firstTxId = 1457634084250832 var firstTxId = 1457634084250832
var result, initialState var result, initialState
before(function(done) { before(function (done) {
initialState = { initialState = {
appState: { appState: {
currentView: { currentView: {
@ -122,42 +116,42 @@ describe('tx confirmation screen', function() {
unapprovedTxs: { unapprovedTxs: {
'1457634084250832': { '1457634084250832': {
id: firstTxId, id: firstTxId,
status: "unconfirmed", status: 'unconfirmed',
time: 1457634084250, time: 1457634084250,
}, },
'1457634084250833': { '1457634084250833': {
id: 1457634084250833, id: 1457634084250833,
status: "unconfirmed", status: 'unconfirmed',
time: 1457634084255, time: 1457634084255,
}, },
}, },
} },
} }
freeze(initialState) freeze(initialState)
// Mocking a background connection: // Mocking a background connection:
actions._setBackgroundConnection({ actions._setBackgroundConnection({
approveTransaction(firstTxId, cb) { cb() }, approveTransaction (firstTxId, cb) { cb() },
}) })
let action = actions.sendTx({id: firstTxId})(function(action) { actions.sendTx({id: firstTxId})(function (action) {
result = reducers(initialState, action) result = reducers(initialState, action)
}) })
done() done()
}) })
it('should stay on the confTx view', function() { it('should stay on the confTx view', function () {
assert.equal(result.appState.currentView.name, 'confTx') assert.equal(result.appState.currentView.name, 'confTx')
}) })
it('should transition to the first tx', function() { it('should transition to the first tx', function () {
assert.equal(result.appState.currentView.context, 0) assert.equal(result.appState.currentView.context, 0)
}) })
}) })
}) })
}); })
function getUnconfirmedTxCount(state) { function getUnconfirmedTxCount (state) {
var txs = state.metamask.unapprovedTxs var txs = state.metamask.unapprovedTxs
var count = Object.keys(txs).length var count = Object.keys(txs).length
return count return count

@ -1,23 +1,22 @@
var jsdom = require('mocha-jsdom') // var jsdom = require('mocha-jsdom')
var assert = require('assert') var assert = require('assert')
var freeze = require('deep-freeze-strict') var freeze = require('deep-freeze-strict')
var path = require('path') var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('SHOW_INFO_PAGE', function() { describe('SHOW_INFO_PAGE', function () {
it('sets the state.appState.currentView.name property to info', function () {
it('sets the state.appState.currentView.name property to info', function() {
var initialState = { var initialState = {
appState: { appState: {
activeAddress: 'foo', activeAddress: 'foo',
} },
} }
freeze(initialState) freeze(initialState)
const action = actions.showInfoPage() const action = actions.showInfoPage()
var resultingState = reducers(initialState, action) var resultingState = reducers(initialState, action)
assert.equal(resultingState.appState.currentView.name, 'info') assert.equal(resultingState.appState.currentView.name, 'info')
}); })
}); })

@ -1,14 +1,13 @@
var jsdom = require('mocha-jsdom') // var jsdom = require('mocha-jsdom')
var assert = require('assert') var assert = require('assert')
var freeze = require('deep-freeze-strict') var freeze = require('deep-freeze-strict')
var path = require('path') var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('action DISPLAY_WARNING', function() { describe('action DISPLAY_WARNING', function () {
it('sets appState.warning to provided value', function () {
it('sets appState.warning to provided value', function() {
var initialState = { var initialState = {
appState: {}, appState: {},
} }
@ -20,5 +19,5 @@ describe('action DISPLAY_WARNING', function() {
const resultingState = reducers(initialState, action) const resultingState = reducers(initialState, action)
assert.equal(resultingState.appState.warning, warningText, 'warning text set') assert.equal(resultingState.appState.warning, warningText, 'warning text set')
}); })
}); })

@ -1,5 +1,4 @@
const assert = require('assert') const assert = require('assert')
const extend = require('xtend')
const AddressBookController = require('../../app/scripts/controllers/address-book') const AddressBookController = require('../../app/scripts/controllers/address-book')
const mockKeyringController = { const mockKeyringController = {
@ -7,21 +6,20 @@ const mockKeyringController = {
getState: function () { getState: function () {
return { return {
identities: { identities: {
'0x0aaa' : { '0x0aaa': {
address: '0x0aaa', address: '0x0aaa',
name: 'owned', name: 'owned',
} },
} },
} }
} },
} },
} }
describe('address-book-controller', function () {
describe('address-book-controller', function() {
var addressBookController var addressBookController
beforeEach(function() { beforeEach(function () {
addressBookController = new AddressBookController({}, mockKeyringController) addressBookController = new AddressBookController({}, mockKeyringController)
}) })

@ -1,24 +1,22 @@
var assert = require('assert') var assert = require('assert')
var BinaryRenderer = require('../../../ui/app/components/binary-renderer') var BinaryRenderer = require('../../../ui/app/components/binary-renderer')
describe('BinaryRenderer', function() { describe('BinaryRenderer', function () {
let binaryRenderer let binaryRenderer
const message = 'Hello, world!' const message = 'Hello, world!'
const buffer = new Buffer(message, 'utf8') const buffer = new Buffer(message, 'utf8')
const hex = buffer.toString('hex') const hex = buffer.toString('hex')
beforeEach(function() { beforeEach(function () {
binaryRenderer = new BinaryRenderer() binaryRenderer = new BinaryRenderer()
}) })
it('recovers message', function() { it('recovers message', function () {
const result = binaryRenderer.hexToText(hex) const result = binaryRenderer.hexToText(hex)
assert.equal(result, message) assert.equal(result, message)
}) })
it('recovers message with hex prefix', function () {
it('recovers message with hex prefix', function() {
const result = binaryRenderer.hexToText('0x' + hex) const result = binaryRenderer.hexToText('0x' + hex)
assert.equal(result, message) assert.equal(result, message)
}) })

@ -0,0 +1,51 @@
var assert = require('assert')
const additions = require('react-testutils-additions')
const h = require('react-hyperscript')
const ReactTestUtils = require('react-addons-test-utils')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
var BnInput = require('../../../ui/app/components/bn-as-decimal-input')
describe('BnInput', function () {
it('can tolerate a gas decimal number at a high precision', function (done) {
const renderer = ReactTestUtils.createRenderer()
let valueStr = '20'
while (valueStr.length < 20) {
valueStr += '0'
}
const value = new BN(valueStr, 10)
const inputStr = '2.3'
let targetStr = '23'
while (targetStr.length < 19) {
targetStr += '0'
}
const target = new BN(targetStr, 10)
const precision = 18 // ether precision
const scale = 18
const props = {
value,
scale,
precision,
onChange: (newBn) => {
assert.equal(newBn.toString(), target.toString(), 'should tolerate increase')
done()
},
}
const inputComponent = h(BnInput, props)
const component = additions.renderIntoDocument(inputComponent)
renderer.render(inputComponent)
const input = additions.find(component, 'input.hex-input')[0]
ReactTestUtils.Simulate.change(input, { preventDefault () {}, target: {
value: inputStr,
checkValidity () { return true } },
})
})
})

@ -0,0 +1,77 @@
const assert = require('assert')
const additions = require('react-testutils-additions')
const h = require('react-hyperscript')
const PendingTx = require('../../../ui/app/components/pending-tx')
const ReactTestUtils = require('react-addons-test-utils')
const ethUtil = require('ethereumjs-util')
describe('PendingTx', function () {
const identities = {
'0xfdea65c8e26263f6d9a1b5de9555d2931a33b826': {
name: 'Main Account 1',
balance: '0x00000000000000056bc75e2d63100000',
},
}
const gasPrice = '0x4A817C800' // 20 Gwei
const txData = {
'id': 5021615666270214,
'time': 1494458763011,
'status': 'unapproved',
'metamaskNetworkId': '1494442339676',
'txParams': {
'from': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826',
'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
'value': '0xde0b6b3a7640000',
gasPrice,
'gas': '0x7b0c'},
'gasLimitSpecified': false,
'estimatedGas': '0x5208',
}
it('should use updated values when edited.', function (done) {
const renderer = ReactTestUtils.createRenderer()
const newGasPrice = '0x77359400'
const props = {
identities,
accounts: identities,
txData,
sendTransaction: (txMeta, event) => {
// Assert changes:
const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice)
assert.notEqual(result, gasPrice, 'gas price should change')
assert.equal(result, newGasPrice, 'gas price assigned.')
done()
},
}
const pendingTxComponent = h(PendingTx, props)
const component = additions.renderIntoDocument(pendingTxComponent)
renderer.render(pendingTxComponent)
const result = renderer.getRenderOutput()
assert.equal(result.type, 'div', 'should create a div')
try {
const input = additions.find(component, '.cell.row input[type="number"]')[1]
ReactTestUtils.Simulate.change(input, {
target: {
value: 2,
checkValidity () { return true },
},
})
const form = additions.find(component, 'form')[0]
form.checkValidity = () => true
form.getFormEl = () => { return { checkValidity () { return true } } }
ReactTestUtils.Simulate.submit(form, { preventDefault () {}, target: { checkValidity () {
return true
} } })
} catch (e) {
console.log('WHAAAA')
console.error(e)
}
})
})

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save