1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Merge branch 'master' into i#1203MainNetSwitch

This commit is contained in:
kumavis 2017-06-12 13:27:04 -07:00 committed by GitHub
commit 27220b7bcd
206 changed files with 12280 additions and 9540 deletions

View File

@ -1 +1,4 @@
{ "presets": ["es2015"] } {
"presets": ["es2015"],
"plugins": ["transform-runtime"]
}

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
builds
development

View File

@ -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

View File

@ -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": {

3
.gitignore vendored
View File

@ -16,3 +16,6 @@ development/bundle.js
builds.zip builds.zip
test/integration/bundle.js test/integration/bundle.js
development/states.js development/states.js
test/background.js
test/bundle.js
test/test-bundle.js

View File

@ -3,9 +3,124 @@
## Current Master ## Current Master
- The default network on installation is now MainNet - 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.
- Fix transition issue from account detail screen.
- Revise buy screen for more modularity.
- Fixed some other small bugs.
## 3.5.2 2017-3-28
- Fix bug where gas estimate totals were sometimes wrong.
- Add link to Kovan Test Faucet instructions on buy view.
- Inject web3 into loaded iFrames.
## 3.5.1 2017-3-27
- Fix edge case where users were unable to enable the notice button if notices were short enough to not require a scrollbar.
## 3.5.0 2017-3-27
- Add better error messages for when a transaction fails on approval
- Allow sending to ENS names in send form on Ropsten. - Allow sending to ENS names in send form on Ropsten.
- Added an address book functionality that remembers the last 15 unique addresses sent to. - Added an address book functionality that remembers the last 15 unique addresses sent to.
- Can now change network to custom RPC URL from lock screen. - Can now change network to custom RPC URL from lock screen.
- Removed support for old, lightwallet based vault. Users who have not opened app in over a month will need to recover with their seed phrase. This will allow Firefox support sooner.
- Fixed bug where spinner wouldn't disappear on incorrect password submission on seed word reveal.
- Polish the private key UI.
- Enforce minimum values for gas price and gas limit.
- Fix bug where total gas was sometimes not live-updated.
- Fix bug where editing gas value could have some abrupt behaviors (#1233)
- Add Kovan as an option on our network list.
- Fixed bug where transactions on other networks would disappear when submitting a transaction on another network.
## 3.4.0 2017-3-8 ## 3.4.0 2017-3-8

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM node:7
MAINTAINER kumavis
# setup app dir
RUN mkdir -p /www/
WORKDIR /www/
# install dependencies
COPY ./package.json /www/package.json
RUN npm install
# copy over app dir
COPY ./ /www/
# run tests
# RUN npm test
# build app
RUN npm run dist
# start server
CMD node mascara/example/server.js

134
README.md
View File

@ -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
To install your locally built extension on Chrome, [follow this guide](http://stackoverflow.com/a/24577660/272576). Requires `mocha` installed. Run `npm install -g mocha`.
The built extension is stored in `./dist/chrome/`. 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`.
## 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. - [How to add custom build to Chrome](./docs/add-to-chrome.md)
- [How to add custom build to Firefox](./docs/add-to-firefox.md)
0. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`. - [How to develop a live-reloading UI](./docs/ui-dev-mode.md)
1. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2). - [Publishing Guide](./docs/publishing.md)
2. Run `gulp dist` (or `gulp zip` if you've already built) - [How to develop an in-browser mocked UI](./docs/ui-mock-mode.md)
3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package. - [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

View File

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.4.0", "version": "3.7.7",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
@ -51,13 +51,14 @@
"scripts/contentscript.js" "scripts/contentscript.js"
], ],
"run_at": "document_start", "run_at": "document_start",
"all_frames": false "all_frames": true
} }
], ],
"permissions": [ "permissions": [
"storage", "storage",
"clipboardWrite", "clipboardWrite",
"http://localhost:8545/" "http://localhost:8545/",
"https://api.cryptonator.com/"
], ],
"web_accessible_resources": [ "web_accessible_resources": [
"scripts/inpage.js" "scripts/inpage.js"

View File

@ -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)

View File

@ -1,15 +1,15 @@
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')
const ExtensionPlatform = require('./platforms/extension')
const Migrator = require('./lib/migrator/') const Migrator = require('./lib/migrator/')
const migrations = require('./migrations/') const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const notification = require('./lib/notifications.js') const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller') const MetamaskController = require('./metamask-controller')
const extension = require('./lib/extension') const extension = require('extensionizer')
const firstTimeState = require('./first-time-state') const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
@ -19,44 +19,42 @@ const log = require('loglevel')
window.log = log window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
const platform = new ExtensionPlatform()
const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager
let popupIsOpen = false let popupIsOpen = false
// state persistence // state persistence
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
// //
@ -68,6 +66,8 @@ function setupController (initState) {
showUnapprovedTx: triggerUi, showUnapprovedTx: triggerUi,
// initial state // initial state
initState, initState,
// platform specific api
platform,
}) })
global.metamaskController = controller global.metamaskController = controller
@ -78,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
} }
@ -114,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) {
@ -131,7 +131,6 @@ function setupController (initState) {
} }
return Promise.resolve() return Promise.resolve()
} }
// //
@ -140,7 +139,7 @@ function setupController (initState) {
// popup trigger // popup trigger
function triggerUi () { function triggerUi () {
if (!popupIsOpen) notification.show() if (!popupIsOpen) notificationManager.showPopup()
} }
// On first install, open a window to MetaMask website to how-it-works. // On first install, open a window to MetaMask website to how-it-works.

View File

@ -1,14 +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 DEFAULT_RPC_URL = TESTNET_RPC_URL const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
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,
rinkeby: RINKEBY_RPC_URL,
}, },
} }

View File

@ -2,7 +2,7 @@ const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong') const PongStream = require('ping-pong-stream/pong')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const ObjectMultiplex = require('./lib/obj-multiplex') const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('./lib/extension') const extension = require('extensionizer')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
@ -61,14 +61,22 @@ 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 () {
return isAllowedSuffix(window.location.href) return doctypeCheck() || suffixCheck()
} }
function isAllowedSuffix (testCase) { function doctypeCheck () {
const doctype = window.document.doctype
if (doctype) {
return doctype.name === 'html'
} else {
return false
}
}
function suffixCheck () {
var prohibitedTypes = ['xml', 'pdf'] var prohibitedTypes = ['xml', 'pdf']
var currentUrl = window.location.href var currentUrl = window.location.href
var currentRegex var currentRegex

View File

@ -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)

View File

@ -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')
}
}) })
} }

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 BN = require('ethereumjs-util').BN 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,16 +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.txProviderUtils = new TxProviderUtil(this.provider) this.query = opts.ethQuery
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 () {
@ -37,7 +43,7 @@ module.exports = class TransactionManager extends EventEmitter {
} }
getNetwork () { getNetwork () {
return this.networkStore.getState().network return this.networkStore.getState()
} }
getSelectedAddress () { getSelectedAddress () {
@ -46,28 +52,41 @@ 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.store.getState().transactions const fullTxList = this.getFullTxList()
return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network)
} }
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
// Returns the full tx list across all networks
getFullTxList () {
return this.store.getState().transactions
}
// Adds a tx to the txlist // Adds a tx to the txlist
addTx (txMeta) { addTx (txMeta) {
var txList = this.getTxList() const txCount = this.getTxCount()
var txHistoryLimit = this.txHistoryLimit const network = this.getNetwork()
const fullTxList = this.getFullTxList()
const txHistoryLimit = this.txHistoryLimit
// checks if the length of th tx history is // checks if the length of the tx history is
// longer then desired persistence limit // longer then desired persistence limit
// and then if it is removes only confirmed // and then if it is removes only confirmed
// or rejected tx's. // or rejected tx's.
// not tx's that are pending or unapproved // not tx's that are pending or unapproved
if (txList.length > txHistoryLimit - 1) { if (txCount > txHistoryLimit - 1) {
var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
txList.splice(index, 1) fullTxList.splice(index, 1)
} }
txList.push(txMeta) fullTxList.push(txMeta)
this._saveTxList(fullTxList)
this.emit('update')
this._saveTxList(txList)
this.once(`${txMeta.id}:signed`, function (txId) { this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`) this.removeAllListeners(`${txMeta.id}:rejected`)
}) })
@ -89,7 +108,7 @@ module.exports = class TransactionManager extends EventEmitter {
// //
updateTx (txMeta) { updateTx (txMeta) {
var txId = txMeta.id var txId = txMeta.id
var txList = this.getTxList() var txList = this.getFullTxList()
var index = txList.findIndex(txData => txData.id === txId) var index = txList.findIndex(txData => txData.id === txId)
txList[index] = txMeta txList[index] = txMeta
this._saveTxList(txList) this._saveTxList(txList)
@ -109,44 +128,38 @@ module.exports = class TransactionManager extends EventEmitter {
async.waterfall([ async.waterfall([
// validate // validate
(cb) => this.txProviderUtils.validateTxParams(txParams, cb), (cb) => this.txProviderUtils.validateTxParams(txParams, cb),
// prepare txMeta // construct txMeta
(cb) => { (cb) => {
// create txMeta obj with parameters and meta data
let time = (new Date()).getTime()
let txId = createId()
txParams.metamaskId = txId
txParams.metamaskNetworkId = this.getNetwork()
txMeta = { txMeta = {
id: txId, id: createId(),
time: time, time: (new Date()).getTime(),
status: 'unapproved', status: 'unapproved',
metamaskNetworkId: this.getNetwork(), metamaskNetworkId: this.getNetwork(),
txParams: txParams, txParams: txParams,
} }
// calculate metadata for tx cb()
this.txProviderUtils.analyzeGasUsage(txMeta, cb)
}, },
// add default tx params
(cb) => this.addTxDefaults(txMeta, cb),
// save txMeta // save txMeta
(cb) => { (cb) => {
this.addTx(txMeta) this.addTx(txMeta)
this.setMaxTxCostAndFee(txMeta)
cb(null, txMeta) cb(null, txMeta)
}, },
], done) ], done)
} }
setMaxTxCostAndFee (txMeta) { addTxDefaults (txMeta, cb) {
var txParams = txMeta.txParams const txParams = txMeta.txParams
var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16) // ensure value
var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16) txParams.value = txParams.value || '0x0'
var txFee = gasCost.mul(gasPrice) this.query.gasPrice((err, gasPrice) => {
var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16) if (err) return cb(err)
var maxCost = txValue.add(txFee) // set gasPrice
txMeta.txFee = txFee txParams.gasPrice = gasPrice
txMeta.txValue = txValue // set gasLimit
txMeta.maxCost = maxCost this.txProviderUtils.analyzeGasUsage(txMeta, cb)
txMeta.gasPrice = gasPrice })
this.updateTx(txMeta)
} }
getUnapprovedTxList () { getUnapprovedTxList () {
@ -172,7 +185,10 @@ module.exports = class TransactionManager extends EventEmitter {
], (err) => { ], (err) => {
self.nonceLock.leave() self.nonceLock.leave()
if (err) { if (err) {
this.setTxStatusFailed(txId) this.setTxStatusFailed(txId, {
errCode: err.errCode || err,
message: err.message || 'Transaction failed during approval',
})
return cb(err) return cb(err)
} }
cb() cb()
@ -186,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)
@ -194,11 +210,23 @@ module.exports = class TransactionManager extends EventEmitter {
}) })
} }
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState.network)
if (Number.isNaN(getChainId)) {
return 0
} else {
return getChainId
}
}
signTransaction (txId, cb) { signTransaction (txId, cb) {
let txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
let txParams = txMeta.txParams const txParams = txMeta.txParams
let fromAddress = txParams.from const fromAddress = txParams.from
let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) // add network/chain id
txParams.chainId = this.getChainId()
const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams)
this.signEthTx(ethTx, fromAddress).then(() => { this.signEthTx(ethTx, fromAddress).then(() => {
this.setTxStatusSigned(txMeta.id) this.setTxStatusSigned(txMeta.id)
cb(null, ethUtil.bufferToHex(ethTx.serialize())) cb(null, ethUtil.bufferToHex(ethTx.serialize()))
@ -207,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)
@ -219,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)
} }
@ -291,7 +323,10 @@ module.exports = class TransactionManager extends EventEmitter {
this._setTxStatus(txId, 'confirmed') this._setTxStatus(txId, 'confirmed')
} }
setTxStatusFailed (txId) { setTxStatusFailed (txId, reason) {
const txMeta = this.getTx(txId)
txMeta.err = reason
this.updateTx(txMeta)
this._setTxStatus(txId, 'failed') this._setTxStatus(txId, 'failed')
} }
@ -312,14 +347,13 @@ 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) {
txMeta.err = { 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.',
} }
this.updateTx(txMeta) return this.setTxStatusFailed(txId, errReason)
return this.setTxStatusFailed(txId)
} }
this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => { this.query.getTransactionByHash(txHash, (err, txParams) => {
if (err || !txParams) { if (err || !txParams) {
if (!txParams) return if (!txParams) return
txMeta.err = { txMeta.err = {
@ -328,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)
@ -354,6 +388,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')
@ -373,7 +408,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')

View File

@ -6,9 +6,11 @@ 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: (METAMASK_DEBUG || env === 'test') ? 'testnet' : 'mainnet', type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet',
type: 'rinkeby',
}, },
}, },
} }

View File

@ -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
}) })

View File

@ -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)
@ -324,6 +324,7 @@ class KeyringController extends EventEmitter {
if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
const hexAccount = normalizeAddress(firstAccount) const hexAccount = normalizeAddress(firstAccount)
this.emit('newAccount', hexAccount) this.emit('newAccount', hexAccount)
this.emit('newVault', hexAccount)
return this.setupAccounts(accounts) return this.setupAccounts(accounts)
}) })
.then(this.persistAllKeyrings.bind(this)) .then(this.persistAllKeyrings.bind(this))
@ -581,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 })

View File

@ -3,10 +3,18 @@ const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const env = process.env.METAMASK_ENV const env = process.env.METAMASK_ENV
module.exports = function (address) { module.exports = function (address) {
if (METAMASK_DEBUG || env === 'test') return // Don't faucet in development or test // Don't faucet in development or test
var http = new XMLHttpRequest() if (METAMASK_DEBUG === true || env === 'test') return
var data = address global.log.info('auto-fauceting:', address)
http.open('POST', uri, true) const data = address
http.setRequestHeader('Content-type', 'application/rawdata') const headers = new Headers()
http.send(data) headers.append('Content-type', 'application/rawdata')
fetch(uri, {
method: 'POST',
headers,
body: data,
})
.catch((err) => {
console.error(err)
})
} }

View File

@ -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

View File

@ -0,0 +1,23 @@
module.exports = getBuyEthUrl
function getBuyEthUrl ({ network, amount, address }) {
let url
switch (network) {
case '1':
url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
break
case '3':
url = 'https://faucet.metamask.io/'
break
case '4':
url = 'https://www.rinkeby.io/'
break
case '42':
url = 'https://github.com/kovan-testnet/faucet'
break
}
return url
}

View File

@ -1,10 +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 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.
@ -32,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)
} }
@ -135,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()
@ -144,14 +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': case 'kovan':
return MORDEN_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
} }
} }

View File

@ -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 {
@ -19,6 +19,9 @@ class EthereumStore extends ObservableStore {
super({ super({
accounts: {}, accounts: {},
transactions: {}, transactions: {},
currentBlockNumber: '0',
currentBlockHash: '',
currentBlockGasLimit: '',
}) })
this._provider = opts.provider this._provider = opts.provider
this._query = new EthQuery(this._provider) this._query = new EthQuery(this._provider)
@ -69,6 +72,9 @@ class EthereumStore extends ObservableStore {
_updateForBlock (block) { _updateForBlock (block) {
const blockNumber = '0x' + block.number.toString('hex') const blockNumber = '0x' + block.number.toString('hex')
this._currentBlockNumber = blockNumber this._currentBlockNumber = blockNumber
this.updateState({ currentBlockNumber: parseInt(blockNumber) })
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),
@ -129,4 +135,4 @@ class EthereumStore extends ObservableStore {
} }
module.exports = EthereumStore module.exports = EthereumStore

View File

@ -1,68 +0,0 @@
const apis = [
'alarms',
'bookmarks',
'browserAction',
'commands',
'contextMenus',
'cookies',
'downloads',
'events',
'extension',
'extensionTypes',
'history',
'i18n',
'idle',
'notifications',
'pageAction',
'runtime',
'storage',
'tabs',
'webNavigation',
'webRequest',
'windows',
]
function Extension () {
const _this = this
apis.forEach(function (api) {
_this[api] = null
try {
if (chrome[api]) {
_this[api] = chrome[api]
}
} catch (e) {}
try {
if (window[api]) {
_this[api] = window[api]
}
} catch (e) {}
try {
if (browser[api]) {
_this[api] = browser[api]
}
} catch (e) {}
try {
_this.api = browser.extension[api]
} catch (e) {}
})
try {
if (browser && browser.runtime) {
this.runtime = browser.runtime
}
} catch (e) {}
try {
if (browser && browser.browserAction) {
this.browserAction = browser.browserAction
}
} catch (e) {}
}
module.exports = Extension

View File

@ -1,14 +0,0 @@
/* Extension.js
*
* A module for unifying browser differences in the WebExtension API.
*
* Initially implemented because Chrome hides all of their WebExtension API
* behind a global `chrome` variable, but we'd like to start grooming
* the code-base for cross-browser extension support.
*
* You can read more about the WebExtension API here:
* https://developer.mozilla.org/en-US/Add-ons/WebExtensions
*/
const Extension = require('./extension-instance')
module.exports = new Extension()

View File

@ -0,0 +1,7 @@
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
module.exports = function hexToBn (hex) {
return new BN(ethUtil.stripHexPrefix(hex), 16)
}

View File

@ -1,90 +0,0 @@
/* ID Management
*
* This module exists to hold the decrypted credentials for the current session.
* It therefore exposes sign methods, because it is able to perform these
* with noa dditional authentication, because its very instantiation
* means the vault is unlocked.
*/
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
module.exports = IdManagement
function IdManagement (opts) {
if (!opts) opts = {}
this.keyStore = opts.keyStore
this.derivedKey = opts.derivedKey
this.configManager = opts.configManager
this.hdPathString = "m/44'/60'/0'/0"
this.getAddresses = function () {
return this.keyStore.getAddresses(this.hdPathString).map(function (address) { return '0x' + address })
}
this.signTx = function (txParams) {
// normalize values
txParams.gasPrice = ethUtil.intToHex(txParams.gasPrice)
txParams.to = ethUtil.addHexPrefix(txParams.to)
txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase())
txParams.value = ethUtil.addHexPrefix(txParams.value)
txParams.data = ethUtil.addHexPrefix(txParams.data)
txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas)
txParams.nonce = ethUtil.addHexPrefix(txParams.nonce)
var tx = new Transaction(txParams)
// sign tx
var privKeyHex = this.exportPrivateKey(txParams.from)
var privKey = ethUtil.toBuffer(privKeyHex)
tx.sign(privKey)
// Add the tx hash to the persisted meta-tx object
var txHash = ethUtil.bufferToHex(tx.hash())
var metaTx = this.configManager.getTx(txParams.metamaskId)
metaTx.hash = txHash
this.configManager.updateTx(metaTx)
// return raw serialized tx
var rawTx = ethUtil.bufferToHex(tx.serialize())
return rawTx
}
this.signMsg = function (address, message) {
// sign message
var privKeyHex = this.exportPrivateKey(address.toLowerCase())
var privKey = ethUtil.toBuffer(privKeyHex)
var msgSig = ethUtil.ecsign(new Buffer(message.replace('0x', ''), 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s))
return rawMsgSig
}
this.getSeed = function () {
return this.keyStore.getSeed(this.derivedKey)
}
this.exportPrivateKey = function (address) {
var privKeyHex = ethUtil.addHexPrefix(this.keyStore.exportPrivateKey(address, this.derivedKey, this.hdPathString))
return privKeyHex
}
}
function padWithZeroes (number, length) {
var myString = '' + number
while (myString.length < length) {
myString = '0' + myString
}
return myString
}
function concatSig (v, r, s) {
const rSig = ethUtil.fromSigned(r)
const sSig = ethUtil.fromSigned(s)
const vSig = ethUtil.bufferToInt(v)
const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64)
const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64)
const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig))
return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex')
}

View File

@ -1,80 +0,0 @@
const IdentityStore = require('./idStore')
const HdKeyring = require('eth-hd-keyring')
const sigUtil = require('eth-sig-util')
const normalize = sigUtil.normalize
const denodeify = require('denodeify')
module.exports = class IdentityStoreMigrator {
constructor ({ configManager }) {
this.configManager = configManager
const hasOldVault = this.hasOldVault()
if (!hasOldVault) {
this.idStore = new IdentityStore({ configManager })
}
}
migratedVaultForPassword (password) {
const hasOldVault = this.hasOldVault()
const configManager = this.configManager
if (!this.idStore) {
this.idStore = new IdentityStore({ configManager })
}
if (!hasOldVault) {
return Promise.resolve(null)
}
const idStore = this.idStore
const submitPassword = denodeify(idStore.submitPassword.bind(idStore))
return submitPassword(password)
.then(() => {
const serialized = this.serializeVault()
return this.checkForLostAccounts(serialized)
})
}
serializeVault () {
const mnemonic = this.idStore._idmgmt.getSeed()
const numberOfAccounts = this.idStore._getAddresses().length
return {
type: 'HD Key Tree',
data: { mnemonic, numberOfAccounts },
}
}
checkForLostAccounts (serialized) {
const hd = new HdKeyring()
return hd.deserialize(serialized.data)
.then((hexAccounts) => {
const newAccounts = hexAccounts.map(normalize)
const oldAccounts = this.idStore._getAddresses().map(normalize)
const lostAccounts = oldAccounts.reduce((result, account) => {
if (newAccounts.includes(account)) {
return result
} else {
result.push(account)
return result
}
}, [])
return {
serialized,
lostAccounts: lostAccounts.map((address) => {
return {
address,
privateKey: this.idStore.exportAccount(address),
}
}),
}
})
}
hasOldVault () {
const wallet = this.configManager.getWallet()
return wallet
}
}

View File

@ -1,343 +0,0 @@
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const KeyStore = require('eth-lightwallet').keystore
const clone = require('clone')
const extend = require('xtend')
const autoFaucet = require('./auto-faucet')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
const IdManagement = require('./id-management')
module.exports = IdentityStore
inherits(IdentityStore, EventEmitter)
function IdentityStore (opts = {}) {
EventEmitter.call(this)
// we just use the ethStore to auto-add accounts
this._ethStore = opts.ethStore
this.configManager = opts.configManager
// lightwallet key store
this._keyStore = null
// lightwallet wrapper
this._idmgmt = null
this.hdPathString = "m/44'/60'/0'/0"
this._currentState = {
selectedAddress: null,
identities: {},
}
// not part of serilized metamask state - only kept in memory
}
//
// public
//
IdentityStore.prototype.createNewVault = function (password, cb) {
delete this._keyStore
var serializedKeystore = this.configManager.getWallet()
if (serializedKeystore) {
this.configManager.setData({})
}
this.purgeCache()
this._createVault(password, null, (err) => {
if (err) return cb(err)
this._autoFaucet()
this.configManager.setShowSeedWords(true)
var seedWords = this._idmgmt.getSeed()
this._loadIdentities()
cb(null, seedWords)
})
}
IdentityStore.prototype.recoverSeed = function (cb) {
this.configManager.setShowSeedWords(true)
if (!this._idmgmt) return cb(new Error('Unauthenticated. Please sign in.'))
var seedWords = this._idmgmt.getSeed()
cb(null, seedWords)
}
IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) {
this.purgeCache()
this._createVault(password, seed, (err) => {
if (err) return cb(err)
this._loadIdentities()
cb(null, this.getState())
})
}
IdentityStore.prototype.setStore = function (store) {
this._ethStore = store
}
IdentityStore.prototype.clearSeedWordCache = function (cb) {
const configManager = this.configManager
configManager.setShowSeedWords(false)
cb(null, configManager.getSelectedAccount())
}
IdentityStore.prototype.getState = function () {
const configManager = this.configManager
var seedWords = this.getSeedIfUnlocked()
return clone(extend(this._currentState, {
isInitialized: !!configManager.getWallet() && !seedWords,
isUnlocked: this._isUnlocked(),
seedWords: seedWords,
selectedAddress: configManager.getSelectedAccount(),
}))
}
IdentityStore.prototype.getSeedIfUnlocked = function () {
const configManager = this.configManager
var showSeed = configManager.getShouldShowSeedWords()
var idmgmt = this._idmgmt
var shouldShow = showSeed && !!idmgmt
var seedWords = shouldShow ? idmgmt.getSeed() : null
return seedWords
}
IdentityStore.prototype.getSelectedAddress = function () {
const configManager = this.configManager
return configManager.getSelectedAccount()
}
IdentityStore.prototype.setSelectedAddressSync = function (address) {
const configManager = this.configManager
if (!address) {
var addresses = this._getAddresses()
address = addresses[0]
}
configManager.setSelectedAccount(address)
return address
}
IdentityStore.prototype.setSelectedAddress = function (address, cb) {
const resultAddress = this.setSelectedAddressSync(address)
if (cb) return cb(null, resultAddress)
}
IdentityStore.prototype.revealAccount = function (cb) {
const derivedKey = this._idmgmt.derivedKey
const keyStore = this._keyStore
const configManager = this.configManager
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 1)
const addresses = keyStore.getAddresses()
const address = addresses[ addresses.length - 1 ]
this._ethStore.addAccount(ethUtil.addHexPrefix(address))
configManager.setWallet(keyStore.serialize())
this._loadIdentities()
this._didUpdate()
cb(null)
}
IdentityStore.prototype.getNetwork = function (err) {
if (err) {
this._currentState.network = 'loading'
this._didUpdate()
}
this.web3.version.getNetwork((err, network) => {
if (err) {
this._currentState.network = 'loading'
return this._didUpdate()
}
if (global.METAMASK_DEBUG) {
console.log('web3.getNetwork returned ' + network)
}
this._currentState.network = network
this._didUpdate()
})
}
IdentityStore.prototype.setLocked = function (cb) {
delete this._keyStore
delete this._idmgmt
cb()
}
IdentityStore.prototype.submitPassword = function (password, cb) {
const configManager = this.configManager
this.tryPassword(password, (err) => {
if (err) return cb(err)
// load identities before returning...
this._loadIdentities()
cb(null, configManager.getSelectedAccount())
})
}
IdentityStore.prototype.exportAccount = function (address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
if (cb) cb(null, privateKey)
return privateKey
}
// private
//
IdentityStore.prototype._didUpdate = function () {
this.emit('update', this.getState())
}
IdentityStore.prototype._isUnlocked = function () {
var result = Boolean(this._keyStore) && Boolean(this._idmgmt)
return result
}
// load identities from keyStoreet
IdentityStore.prototype._loadIdentities = function () {
const configManager = this.configManager
if (!this._isUnlocked()) throw new Error('not unlocked')
var addresses = this._getAddresses()
addresses.forEach((address, i) => {
// // add to ethStore
if (this._ethStore) {
this._ethStore.addAccount(ethUtil.addHexPrefix(address))
}
// add to identities
const defaultLabel = 'Account ' + (i + 1)
const nickname = configManager.nicknameForWallet(address)
var identity = {
name: nickname || defaultLabel,
address: address,
mayBeFauceting: this._mayBeFauceting(i),
}
this._currentState.identities[address] = identity
})
this._didUpdate()
}
IdentityStore.prototype.saveAccountLabel = function (account, label, cb) {
const configManager = this.configManager
configManager.setNicknameForWallet(account, label)
this._loadIdentities()
cb(null, label)
}
// mayBeFauceting
// If on testnet, index 0 may be fauceting.
// The UI will have to check the balance to know.
// If there is no balance and it mayBeFauceting,
// then it is in fact fauceting.
IdentityStore.prototype._mayBeFauceting = function (i) {
const configManager = this.configManager
var config = configManager.getProvider()
if (i === 0 &&
config.type === 'rpc' &&
config.rpcTarget === DEFAULT_RPC) {
return true
}
return false
}
//
// keyStore managment - unlocking + deserialization
//
IdentityStore.prototype.tryPassword = function (password, cb) {
var serializedKeystore = this.configManager.getWallet()
var keyStore = KeyStore.deserialize(serializedKeystore)
keyStore.keyFromPassword(password, (err, pwDerivedKey) => {
if (err) return cb(err)
const isCorrect = keyStore.isDerivedKeyCorrect(pwDerivedKey)
if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))
this._keyStore = keyStore
this._createIdMgmt(pwDerivedKey)
cb()
})
}
IdentityStore.prototype._createVault = function (password, seedPhrase, cb) {
const opts = {
password,
hdPathString: this.hdPathString,
}
if (seedPhrase) {
opts.seedPhrase = seedPhrase
}
KeyStore.createVault(opts, (err, keyStore) => {
if (err) return cb(err)
this._keyStore = keyStore
keyStore.keyFromPassword(password, (err, derivedKey) => {
if (err) return cb(err)
this.purgeCache()
keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'})
this._createFirstWallet(derivedKey)
this._createIdMgmt(derivedKey)
this.setSelectedAddressSync()
cb()
})
})
}
IdentityStore.prototype._createIdMgmt = function (derivedKey) {
this._idmgmt = new IdManagement({
keyStore: this._keyStore,
derivedKey: derivedKey,
configManager: this.configManager,
})
}
IdentityStore.prototype.purgeCache = function () {
this._currentState.identities = {}
let accounts
try {
accounts = Object.keys(this._ethStore._currentState.accounts)
} catch (e) {
accounts = []
}
accounts.forEach((address) => {
this._ethStore.removeAccount(address)
})
}
IdentityStore.prototype._createFirstWallet = function (derivedKey) {
const keyStore = this._keyStore
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 1)
this.configManager.setWallet(keyStore.serialize())
var addresses = keyStore.getAddresses()
this._ethStore.addAccount(ethUtil.addHexPrefix(addresses[0]))
}
// get addresses and normalize address hexString
IdentityStore.prototype._getAddresses = function () {
return this._keyStore.getAddresses(this.hdPathString).map((address) => {
return ethUtil.addHexPrefix(address)
})
}
IdentityStore.prototype._autoFaucet = function () {
var addresses = this._getAddresses()
autoFaucet(addresses[0])
}
// util

View File

@ -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)

View File

@ -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

View File

@ -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 (
asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration))
.then(() => versionedData)
)
// migration is "pending" if hit has a higher for (let index in pendingMigrations) {
let migration = pendingMigrations[index]
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')
}
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: {

View File

@ -0,0 +1,71 @@
const extension = require('extensionizer')
const height = 520
const width = 360
class NotificationManager {
//
// Public
//
showPopup () {
this._getPopup((err, popup) => {
if (err) throw err
if (popup) {
// bring focus to existing popup
extension.windows.update(popup.id, { focused: true })
} else {
// create new popup
extension.windows.create({
url: 'notification.html',
type: 'popup',
width,
height,
})
}
})
}
closePopup () {
this._getPopup((err, popup) => {
if (err) throw err
if (!popup) return
extension.windows.remove(popup.id, console.error)
})
}
//
// Private
//
_getPopup (cb) {
this._getWindows((err, windows) => {
if (err) throw err
cb(null, this._getPopupIn(windows))
})
}
_getWindows (cb) {
// Ignore in test environment
if (!extension.windows) {
return cb()
}
extension.windows.getAll({}, (windows) => {
cb(null, windows)
})
}
_getPopupIn (windows) {
return windows ? windows.find((win) => {
return (win && win.type === 'popup' &&
win.height === height &&
win.width === width)
}) : null
}
}
module.exports = NotificationManager

View File

@ -1,65 +0,0 @@
const extension = require('./extension')
const height = 520
const width = 360
const notifications = {
show,
getPopup,
closePopup,
}
module.exports = notifications
window.METAMASK_NOTIFIER = notifications
function show () {
getPopup((err, popup) => {
if (err) throw err
if (popup) {
// bring focus to existing popup
extension.windows.update(popup.id, { focused: true })
} else {
// create new popup
extension.windows.create({
url: 'notification.html',
type: 'popup',
focused: true,
width,
height,
})
}
})
}
function getWindows (cb) {
// Ignore in test environment
if (!extension.windows) {
return cb()
}
extension.windows.getAll({}, (windows) => {
cb(null, windows)
})
}
function getPopup (cb) {
getWindows((err, windows) => {
if (err) throw err
cb(null, getPopupIn(windows))
})
}
function getPopupIn (windows) {
return windows ? windows.find((win) => {
return (win && win.type === 'popup' &&
win.height === height &&
win.width === width)
}) : null
}
function closePopup () {
getPopup((err, popup) => {
if (err) throw err
if (!popup) return
extension.windows.remove(popup.id, console.error)
})
}

View File

@ -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)) {

View File

@ -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,53 +6,55 @@ 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) {
this.provider = provider constructor (ethQuery) {
this.query = new EthQuery(provider) this.query = ethQuery
} }
analyzeGasUsage (txData, cb) { analyzeGasUsage (txMeta, cb) {
var self = this var self = this
this.query.getBlockByNumber('latest', true, (err, block) => { this.query.getBlockByNumber('latest', true, (err, block) => {
if (err) return cb(err) if (err) return cb(err)
async.waterfall([ async.waterfall([
self.estimateTxGas.bind(self, txData, block.gasLimit), self.estimateTxGas.bind(self, txMeta, block.gasLimit),
self.setTxGas.bind(self, txData, block.gasLimit), self.setTxGas.bind(self, txMeta, block.gasLimit),
], cb) ], cb)
}) })
} }
estimateTxGas (txData, blockGasLimitHex, cb) { estimateTxGas (txMeta, blockGasLimitHex, cb) {
const txParams = txData.txParams const txParams = txMeta.txParams
// check if gasLimit is already specified // check if gasLimit is already specified
txData.gasLimitSpecified = Boolean(txParams.gas) txMeta.gasLimitSpecified = Boolean(txParams.gas)
// if not, fallback to block gasLimit // if not, fallback to block gasLimit
if (!txData.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)
} }
setTxGas (txData, blockGasLimitHex, estimatedGasHex, cb) { setTxGas (txMeta, blockGasLimitHex, estimatedGasHex, cb) {
txData.estimatedGas = estimatedGasHex txMeta.estimatedGas = estimatedGasHex
const txParams = txData.txParams const txParams = txMeta.txParams
// if gasLimit was specified and doesnt OOG, // if gasLimit was specified and doesnt OOG,
// use original specified amount // use original specified amount
if (txData.gasLimitSpecified) { if (txMeta.gasLimitSpecified) {
txData.estimatedGas = txParams.gas txMeta.estimatedGas = txParams.gas
cb() cb()
return return
} }
// if gasLimit not originally specified, // if gasLimit not originally specified,
// try adding an additional gas buffer to our estimation for safety // try adding an additional gas buffer to our estimation for safety
const recommendedGasHex = this.addGasBuffer(txData.estimatedGas, blockGasLimitHex) const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex)
txParams.gas = recommendedGasHex txParams.gas = recommendedGasHex
cb() cb()
return return
@ -62,25 +63,26 @@ module.exports = class txProviderUtils {
addGasBuffer (initialGasLimitHex, blockGasLimitHex) { addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
const initialGasLimitBn = hexToBn(initialGasLimitHex) const initialGasLimitBn = hexToBn(initialGasLimitHex)
const blockGasLimitBn = hexToBn(blockGasLimitHex) const blockGasLimitBn = hexToBn(blockGasLimitHex)
const upperGasLimitBn = blockGasLimitBn.muln(0.9)
const bufferedGasLimitBn = initialGasLimitBn.muln(1.5) const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)
// if initialGasLimit is above blockGasLimit, dont modify it // if initialGasLimit is above blockGasLimit, dont modify it
if (initialGasLimitBn.gt(blockGasLimitBn)) return bnToHex(initialGasLimitBn) if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn)
// if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
if (bufferedGasLimitBn.lt(blockGasLimitBn)) return bnToHex(bufferedGasLimitBn) if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn)
// otherwise use blockGasLimit // otherwise use blockGasLimit
return bnToHex(blockGasLimitBn) return bnToHex(upperGasLimitBn)
} }
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)
@ -90,16 +92,13 @@ module.exports = class txProviderUtils {
// builds ethTx from txParams object // builds ethTx from txParams object
buildEthTxFromParams (txParams) { buildEthTxFromParams (txParams) {
// apply gas multiplyer
let gasPrice = hexToBn(txParams.gasPrice)
// multiply and divide by 100 so as to add percision to integer mul
txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
// normalize values // normalize values
txParams.to = normalize(txParams.to) txParams.to = normalize(txParams.to)
txParams.from = normalize(txParams.from) txParams.from = normalize(txParams.from)
txParams.value = normalize(txParams.value) txParams.value = normalize(txParams.value)
txParams.data = normalize(txParams.data) txParams.data = normalize(txParams.data)
txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas) txParams.gas = normalize(txParams.gas || txParams.gasLimit)
txParams.gasPrice = normalize(txParams.gasPrice)
txParams.nonce = normalize(txParams.nonce) txParams.nonce = normalize(txParams.nonce)
// build ethTx // build ethTx
log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`) log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`)
@ -124,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)
}

View File

@ -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,13 +17,12 @@ 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 extension = require('./lib/extension')
const autoFaucet = require('./lib/auto-faucet') const autoFaucet = require('./lib/auto-faucet')
const nodeify = require('./lib/nodeify') const nodeify = require('./lib/nodeify')
const IdStoreMigrator = require('./lib/idStore-migrator')
const accountImporter = require('./account-import-strategies') const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
const version = require('../manifest.json').version const version = require('../manifest.json').version
@ -33,14 +31,17 @@ 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
this.platform = opts.platform
// observable state store // observable state store
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,
@ -60,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)
@ -74,10 +73,12 @@ 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)
})
this.keyringController.on('newVault', (address) => {
autoFaucet(address) autoFaucet(address)
}) })
@ -87,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
@ -110,19 +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()
// TEMPORARY UNTIL FULL DEPRECATION:
this.idStoreMigrator = new IdStoreMigrator({
configManager: this.configManager,
})
// 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 })
@ -142,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))
@ -162,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
@ -183,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
} }
@ -214,7 +215,6 @@ module.exports = class MetamaskController extends EventEmitter {
// //
getState () { getState () {
const wallet = this.configManager.getWallet() const wallet = this.configManager.getWallet()
const vault = this.keyringController.store.getState().vault const vault = this.keyringController.store.getState().vault
const isInitialized = (!!wallet || !!vault) const isInitialized = (!!wallet || !!vault)
@ -222,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(),
@ -249,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),
} }
} }
@ -344,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)
}
} }
} }
@ -366,8 +363,7 @@ module.exports = class MetamaskController extends EventEmitter {
// //
submitPassword (password, cb) { submitPassword (password, cb) {
this.migrateOldVaultIfAny(password) return this.keyringController.submitPassword(password)
.then(this.keyringController.submitPassword.bind(this.keyringController, password))
.then((newState) => { cb(null, newState) }) .then((newState) => { cb(null, newState) })
.catch((reason) => { cb(reason) }) .catch((reason) => { cb(reason) })
} }
@ -393,7 +389,7 @@ module.exports = class MetamaskController extends EventEmitter {
.then((serialized) => { .then((serialized) => {
const seedWords = serialized.mnemonic const seedWords = serialized.mnemonic
this.configManager.setSeedWords(seedWords) this.configManager.setSeedWords(seedWords)
cb() cb(null, seedWords)
}) })
} }
@ -425,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)
@ -444,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) => {
@ -464,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) => {
@ -479,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) {
@ -505,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') {
@ -515,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) => {
@ -548,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') {
@ -562,42 +558,13 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, this.getState()) cb(null, this.getState())
} }
// Migrate Old Vault If Any restoreOldVaultAccounts (migratorOutput) {
// @string password
//
// returns Promise()
//
// Temporary step used when logging in.
// Checks if old style (pre-3.0.0) Metamask Vault exists.
// If so, persists that vault in the new vault format
// with the provided password, so the other unlock steps
// may be completed without interruption.
migrateOldVaultIfAny (password) {
if (!this.checkIfShouldMigrate()) {
return Promise.resolve(password)
}
const keyringController = this.keyringController
return this.idStoreMigrator.migratedVaultForPassword(password)
.then(this.restoreOldVaultAccounts.bind(this))
.then(this.restoreOldLostAccounts.bind(this))
.then(keyringController.persistAllKeyrings.bind(keyringController, password))
.then(() => password)
}
checkIfShouldMigrate() {
return !!this.configManager.getWallet() && !this.configManager.getVault()
}
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))
@ -623,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 {
@ -647,91 +608,29 @@ module.exports = class MetamaskController extends EventEmitter {
buyEth (address, amount) { buyEth (address, amount) {
if (!amount) amount = '5' if (!amount) amount = '5'
const network = this.networkController.getNetworkState()
const network = this.getNetworkState() const url = getBuyEthUrl({ network, address, amount })
let url if (url) this.platform.openWindow({ url })
switch (network) {
case '1':
url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
break
case '3':
url = 'https://faucet.metamask.io/'
break
}
if (url) extension.tabs.create({ url })
} }
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')
extension.runtime.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) return this.preferencesController.updateFrequentRpcList(rpcTarget)
.then(() => { .then(() => {
extension.runtime.reload() return Promise.resolve(rpcTarget)
this.lookupNetwork()
return Promise.resolve(rpcTarget)
})
}
setProviderType (type) {
this.configManager.setProviderType(type)
extension.runtime.reload()
this.lookupNetwork()
}
useEtherscanProvider () {
this.configManager.useEtherscanProvider()
extension.runtime.reload()
}
getNetworkState () {
return this.networkStore.getState().network
}
setNetworkState (network) {
return this.networkStore.updateState({ network })
}
isNetworkLoading () {
return this.getNetworkState() === 'loading'
}
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)
}) })
} }
} }

View File

@ -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') {

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,36 @@
const version = 12
/*
This migration modifies our notices to delete their body after being read.
*/
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.NoticeController.noticesList.forEach((notice) => {
if (notice.read) {
notice.body = ''
}
})
return newState
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}) })
}) })
}, },
} }

View File

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

View File

@ -41,6 +41,7 @@ module.exports = class NoticeController extends EventEmitter {
var notices = this.getNoticesList() var notices = this.getNoticesList()
var index = notices.findIndex((currentNotice) => currentNotice.id === noticeToMark.id) var index = notices.findIndex((currentNotice) => currentNotice.id === noticeToMark.id)
notices[index].read = true notices[index].read = true
notices[index].body = ''
this.setNoticesList(notices) this.setNoticesList(notices)
const latestNotice = this.getLatestUnreadNotice() const latestNotice = this.getLatestUnreadNotice()
cb(null, latestNotice) cb(null, latestNotice)

View File

@ -0,0 +1,23 @@
const extension = require('extensionizer')
class ExtensionPlatform {
//
// Public
//
reload () {
extension.runtime.reload()
}
openWindow ({ url }) {
extension.tabs.create({ url })
}
getVersion () {
return extension.runtime.getManifest().version
}
}
module.exports = ExtensionPlatform

View File

@ -0,0 +1,24 @@
class SwPlatform {
//
// Public
//
reload () {
// you cant actually do this
global.location.reload()
}
openWindow ({ url }) {
// this doesnt actually work
global.open(url, '_blank')
}
getVersion () {
return '<unable to read version>'
}
}
module.exports = SwPlatform

View File

@ -0,0 +1,22 @@
class WindowPlatform {
//
// Public
//
reload () {
global.location.reload()
}
openWindow ({ url }) {
global.open(url, '_blank')
}
getVersion () {
return '<unable to read version>'
}
}
module.exports = WindowPlatform

View File

@ -1,7 +1,8 @@
const EventEmitter = require('events').EventEmitter const EventEmitter = require('events').EventEmitter
const async = require('async')
const Dnode = require('dnode') const Dnode = require('dnode')
const Web3 = require('web3') const EthQuery = require('eth-query')
const MetaMaskUi = 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
@ -9,9 +10,12 @@ const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
module.exports = initializePopup module.exports = initializePopup
function initializePopup (connectionStream) { function initializePopup ({ container, connectionStream }, cb) {
// setup app // setup app
connectToAccountManager(connectionStream, setupApp) async.waterfall([
(cb) => connectToAccountManager(connectionStream, cb),
(accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb),
], cb)
} }
function connectToAccountManager (connectionStream, cb) { function connectToAccountManager (connectionStream, cb) {
@ -28,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) {
@ -47,19 +52,3 @@ function setupControllerConnection (connectionStream, cb) {
cb(null, accountManager) cb(null, accountManager)
}) })
} }
function setupApp (err, accountManager) {
var container = document.getElementById('app-content')
if (err) {
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'
log.error(err.stack)
throw err
}
MetaMaskUi({
container: container,
accountManager: accountManager,
})
}

View File

@ -3,23 +3,47 @@ const MetaMaskUiCss = require('../../ui/css')
const startPopup = require('./popup-core') const startPopup = require('./popup-core')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const isPopupOrNotification = require('./lib/is-popup-or-notification') const isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('./lib/extension') const extension = require('extensionizer')
const notification = require('./lib/notifications') const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
var css = MetaMaskUiCss() // create platform global
global.platform = new ExtensionPlatform()
// inject css
const css = MetaMaskUiCss()
injectCss(css) injectCss(css)
var name = isPopupOrNotification() // identify window type (popup, notification)
closePopupIfOpen(name) const windowType = isPopupOrNotification()
window.METAMASK_UI_TYPE = name global.METAMASK_UI_TYPE = windowType
closePopupIfOpen(windowType)
var pluginPort = extension.runtime.connect({ name }) // setup stream to background
var portStream = new PortStream(pluginPort) const extensionPort = extension.runtime.connect({ name: windowType })
const connectionStream = new PortStream(extensionPort)
startPopup(portStream) // start ui
const container = document.getElementById('app-content')
startPopup({ container, connectionStream }, (err, store) => {
if (err) return displayCriticalError(err)
store.subscribe(() => {
const state = store.getState()
if (state.appState.shouldClose) notificationManager.closePopup()
})
})
function closePopupIfOpen (name) {
if (name !== 'notification') { function closePopupIfOpen (windowType) {
notification.closePopup() if (windowType !== 'notification') {
notificationManager.closePopup()
} }
} }
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.style.height = '80px'
log.error(err.stack)
throw err
}

View File

@ -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"

View File

@ -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)

View File

@ -4,7 +4,7 @@
* and stubbing out all the extension methods with appropriate mocks. * and stubbing out all the extension methods with appropriate mocks.
*/ */
const extension = require('../app/scripts/lib/extension') const extension = require('extensionizer')
const noop = function () {} const noop = function () {}
const apis = [ const apis = [

View File

@ -0,0 +1,70 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x07284e146926a4facd0ea60598dc4f001ad620f1": {
"address": "0x07284e146926a4facd0ea60598dc4f001ad620f1",
"name": "Account 1"
}
},
"unapprovedTxs": {},
"noActiveNotices": true,
"frequentRpcList": [],
"addressBook": [],
"network": "3",
"accounts": {
"0x07284e146926a4facd0ea60598dc4f001ad620f1": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0x07284e146926a4facd0ea60598dc4f001ad620f1"
}
},
"transactions": {},
"selectedAddressTxList": [],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0,
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree"
],
"keyrings": [
{
"type": "HD Key Tree",
"accounts": [
"07284e146926a4facd0ea60598dc4f001ad620f1"
]
}
],
"selectedAddress": "0x07284e146926a4facd0ea60598dc4f001ad620f1",
"currentCurrency": "USD",
"conversionRate": 43.35903476,
"conversionDate": 1490105102,
"provider": {
"type": "testnet"
},
"shapeShiftTxList": [],
"lostAccounts": [],
"seedWords": null
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"context": "0x07284e146926a4facd0ea60598dc4f001ad620f1"
},
"accountDetail": {
"subview": "export",
"accountExport": "completed",
"privateKey": "549c9638ad06432568969accacad4a02f8548cc358085938071745138ec134b7"
},
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -0,0 +1,69 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x07284e146926a4facd0ea60598dc4f001ad620f1": {
"address": "0x07284e146926a4facd0ea60598dc4f001ad620f1",
"name": "Account 1"
}
},
"unapprovedTxs": {},
"noActiveNotices": true,
"frequentRpcList": [],
"addressBook": [],
"network": "3",
"accounts": {
"0x07284e146926a4facd0ea60598dc4f001ad620f1": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0x07284e146926a4facd0ea60598dc4f001ad620f1"
}
},
"transactions": {},
"selectedAddressTxList": [],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0,
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree"
],
"keyrings": [
{
"type": "HD Key Tree",
"accounts": [
"07284e146926a4facd0ea60598dc4f001ad620f1"
]
}
],
"selectedAddress": "0x07284e146926a4facd0ea60598dc4f001ad620f1",
"currentCurrency": "USD",
"conversionRate": 43.35903476,
"conversionDate": 1490105102,
"provider": {
"type": "testnet"
},
"shapeShiftTxList": [],
"lostAccounts": [],
"seedWords": null
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"context": "0x07284e146926a4facd0ea60598dc4f001ad620f1"
},
"accountDetail": {
"subview": "export",
"accountExport": "requested"
},
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
metamascara:
build: ./
restart: always
ports:
- "9001"
environment:
MASCARA_ORIGIN: "https://zero.metamask.io"
VIRTUAL_PORT: "9001"
VIRTUAL_HOST: "zero.metamask.io"
LETSENCRYPT_HOST: "zero.metamask.io"
LETSENCRYPT_EMAIL: "admin@metamask.io"

14
docs/add-to-chrome.md Normal file
View File

@ -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.

14
docs/add-to-firef.md Normal file
View File

@ -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`.

View File

@ -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

View File

@ -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!

View File

@ -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
```

15
docs/notices.md Normal file
View File

@ -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.

19
docs/publishing.md Normal file
View File

@ -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.

6
docs/ui-dev-mode.md Normal file
View File

@ -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.

8
docs/ui-mock-mode.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -1,24 +0,0 @@
start the dual servers (dapp + mascara)
```
node server.js
```
open the example dapp at `http://localhost:9002/`
*You will need to build MetaMask in order for this to work*
```
gulp dev
```
to build MetaMask and have it live reload if you make changes
## First time use:
- navigate to: http://127.0.0.1:9001/popup/popup.html
- Create an Account
- go back to http://localhost:9002/
- open devTools
- click Sync Tx
### Todos
- Look into using [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)

View File

@ -1,159 +0,0 @@
const urlUtil = require('url')
const extend = require('xtend')
const Dnode = require('dnode')
const eos = require('end-of-stream')
const ParentStream = require('iframe-stream').ParentStream
const PortStream = require('../app/scripts/lib/port-stream.js')
const notification = require('../app/scripts/lib/notifications.js')
const messageManager = require('../app/scripts/lib/message-manager')
const setupMultiplex = require('../app/scripts/lib/stream-utils.js').setupMultiplex
const MetamaskController = require('../app/scripts/metamask-controller')
const extension = require('../app/scripts/lib/extension')
const STORAGE_KEY = 'metamask-config'
initializeZeroClient()
function initializeZeroClient() {
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage,
unlockAccountMessage,
showUnapprovedTx,
// Persistence Methods:
setData,
loadData,
})
const idStore = controller.idStore
function unlockAccountMessage () {
console.log('notif stub - unlockAccountMessage')
}
function showUnconfirmedMessage (msgParams, msgId) {
console.log('notif stub - showUnconfirmedMessage')
}
function showUnapprovedTx (txParams, txData, onTxDoneCb) {
console.log('notif stub - showUnapprovedTx')
}
//
// connect to other contexts
//
var connectionStream = new ParentStream()
connectRemote(connectionStream, getParentHref())
function connectRemote (connectionStream, originDomain) {
var isMetaMaskInternalProcess = (originDomain === '127.0.0.1:9001')
if (isMetaMaskInternalProcess) {
// communication with popup
setupTrustedCommunication(connectionStream, 'MetaMask')
} else {
// communication with page
setupUntrustedCommunication(connectionStream, originDomain)
}
}
function setupUntrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
function setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
setupControllerConnection(mx.createStream('controller'))
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
}
//
// remote features
//
function setupControllerConnection (stream) {
controller.stream = stream
var api = controller.getApi()
var dnode = Dnode(api)
stream.pipe(dnode).pipe(stream)
dnode.on('remote', (remote) => {
// push updates to popup
controller.ethStore.on('update', controller.sendUpdate.bind(controller))
controller.listeners.push(remote)
idStore.on('update', controller.sendUpdate.bind(controller))
// teardown on disconnect
eos(stream, () => {
controller.ethStore.removeListener('update', controller.sendUpdate.bind(controller))
})
})
}
function loadData () {
var oldData = getOldStyleData()
var newData
try {
newData = JSON.parse(window.localStorage[STORAGE_KEY])
} catch (e) {}
var data = extend({
meta: {
version: 0,
},
data: {
config: {
provider: {
type: 'testnet',
},
},
},
}, oldData || null, newData || null)
return data
}
function getOldStyleData () {
var config, wallet, seedWords
var result = {
meta: { version: 0 },
data: {},
}
try {
config = JSON.parse(window.localStorage['config'])
result.data.config = config
} catch (e) {}
try {
wallet = JSON.parse(window.localStorage['lightwallet'])
result.data.wallet = wallet
} catch (e) {}
try {
seedWords = window.localStorage['seedWords']
result.data.seedWords = seedWords
} catch (e) {}
return result
}
function setData (data) {
window.localStorage[STORAGE_KEY] = JSON.stringify(data)
}
function getParentHref(){
try {
var parentLocation = window.parent.location
return parentLocation.hostname + ':' + parentLocation.port
} catch (err) {
return 'unknown'
}
}
}

View File

@ -1,43 +0,0 @@
const Web3 = require('web3')
const setupProvider = require('./lib/setup-provider.js')
//
// setup web3
//
var provider = setupProvider()
hijackProvider(provider)
var web3 = new Web3(provider)
web3.setProvider = function(){
console.log('MetaMask - overrode web3.setProvider')
}
console.log('metamask lib hijacked provider')
//
// export web3
//
global.web3 = web3
//
// ui stuff
//
var shouldPop = false
window.addEventListener('click', function(){
if (!shouldPop) return
shouldPop = false
window.open('http://127.0.0.1:9001/popup/popup.html', '', 'width=360 height=500')
console.log('opening window...')
})
function hijackProvider(provider){
var _super = provider.sendAsync.bind(provider)
provider.sendAsync = function(payload, cb){
if (payload.method === 'eth_sendTransaction') {
console.log('saw send')
shouldPop = true
}
_super(payload, cb)
}
}

View File

@ -1,19 +0,0 @@
const injectCss = require('inject-css')
const MetaMaskUiCss = require('../ui/css')
const startPopup = require('../app/scripts/popup-core')
const setupIframe = require('./lib/setup-iframe.js')
var css = MetaMaskUiCss()
injectCss(css)
var name = 'popup'
window.METAMASK_UI_TYPE = name
var iframeStream = setupIframe({
zeroClientProvider: 'http://127.0.0.1:9001',
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
container: document.body,
})
startPopup(iframeStream)

View File

@ -1,96 +0,0 @@
const express = require('express')
const browserify = require('browserify')
const watchify = require('watchify')
const babelify = require('babelify')
const zeroBundle = createBundle('./index.js')
const controllerBundle = createBundle('./controller.js')
const popupBundle = createBundle('./popup.js')
const appBundle = createBundle('./example/index.js')
//
// Iframe Server
//
const iframeServer = express()
// serve popup window
iframeServer.get('/popup/scripts/popup.js', function(req, res){
res.send(popupBundle.latest)
})
iframeServer.use('/popup', express.static('../dist/chrome'))
// serve controller bundle
iframeServer.get('/controller.js', function(req, res){
res.send(controllerBundle.latest)
})
// serve background controller
iframeServer.use(express.static('./server'))
// start the server
const mascaraPort = 9001
iframeServer.listen(mascaraPort)
console.log(`Mascara service listening on port ${mascaraPort}`)
//
// Dapp Server
//
const dappServer = express()
// serve metamask-lib bundle
dappServer.get('/zero.js', function(req, res){
res.send(zeroBundle.latest)
})
// serve dapp bundle
dappServer.get('/app.js', function(req, res){
res.send(appBundle.latest)
})
// serve static
dappServer.use(express.static('./example'))
// start the server
const dappPort = '9002'
dappServer.listen(dappPort)
console.log(`Dapp listening on port ${dappPort}`)
//
// util
//
function serveBundle(entryPoint){
const bundle = createBundle(entryPoint)
return function(req, res){
res.send(bundle.latest)
}
}
function createBundle(entryPoint){
var bundleContainer = {}
var bundler = browserify({
entries: [entryPoint],
cache: {},
packageCache: {},
plugin: [watchify],
})
bundler.on('update', bundle)
bundle()
return bundleContainer
function bundle() {
bundler.bundle(function(err, result){
if (err) throw err
console.log(`Bundle updated! (${entryPoint})`)
bundleContainer.latest = result.toString()
})
}
}

33
mascara/README.md Normal file
View File

@ -0,0 +1,33 @@
start the dual servers (dapp + mascara)
```
npm run mascara
```
### First time use:
- navigate to: http://localhost:9001
- Create an Account
- go back to http://localhost:9002
- open devTools
- click Sync Tx
### Tests:
```
npm run testMascara
```
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
```

View File

@ -1,5 +1,5 @@
window.addEventListener('load', web3Detect) window.addEventListener('load', web3Detect)
window.addEventListener('message', console.warn)
function web3Detect() { function web3Detect() {
if (global.web3) { if (global.web3) {
@ -13,10 +13,10 @@ function web3Detect() {
function startApp(){ function startApp(){
console.log('app started') console.log('app started')
var primaryAccount = null var primaryAccount
console.log('getting main account...') console.log('getting main account...')
web3.eth.getAccounts(function(err, addresses){ web3.eth.getAccounts((err, addresses) => {
if (err) throw err if (err) console.error(err)
console.log('set address', addresses[0]) console.log('set address', addresses[0])
primaryAccount = addresses[0] primaryAccount = addresses[0]
}) })
@ -24,6 +24,7 @@ function startApp(){
document.querySelector('.action-button-1').addEventListener('click', function(){ document.querySelector('.action-button-1').addEventListener('click', function(){
console.log('saw click') console.log('saw click')
console.log('sending tx') console.log('sending tx')
primaryAccount
web3.eth.sendTransaction({ web3.eth.sendTransaction({
from: primaryAccount, from: primaryAccount,
to: primaryAccount, to: primaryAccount,
@ -53,4 +54,4 @@ function startApp(){
function logToDom(message){ function logToDom(message){
document.body.appendChild(document.createTextNode(message)) document.body.appendChild(document.createTextNode(message))
console.log(message) console.log(message)
} }

View File

@ -3,15 +3,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>MetaMask ZeroClient Example</title> <title>MetaMask ZeroClient Example</title>
<script src="http://localhost:9001/metamascara.js"></script>
</head> </head>
<body> <body>
<button class="action-button-1">SYNC TX</button> <button class="action-button-1">SYNC TX</button>
<button class="action-button-2">ASYNC TX</button> <button class="action-button-2">ASYNC TX</button>
<script src="./zero.js"></script>
<script src="./app.js"></script> <script src="./app.js"></script>
</body> </body>
</html> </html>

31
mascara/example/server.js Normal file
View File

@ -0,0 +1,31 @@
const express = require('express')
const createMetamascaraServer = require('../server/')
const createBundle = require('../server/util').createBundle
const serveBundle = require('../server/util').serveBundle
//
// Iframe Server
//
const mascaraServer = createMetamascaraServer()
// start the server
const mascaraPort = 9001
mascaraServer.listen(mascaraPort)
console.log(`Mascara service listening on port ${mascaraPort}`)
//
// Dapp Server
//
const dappServer = express()
// serve dapp bundle
serveBundle(dappServer, '/app.js', createBundle(require.resolve('./app.js')))
dappServer.use(express.static(__dirname + '/app/'))
// start the server
const dappPort = '9002'
dappServer.listen(dappPort)
console.log(`Dapp listening on port ${dappPort}`)

View File

@ -15,6 +15,6 @@
<body> <body>
Hello! I am the MetaMask iframe. Hello! I am the MetaMask iframe.
<script src="/controller.js"></script> <script src="./proxy.js"></script>
</body> </body>
</html> </html>

32
mascara/server/index.js Normal file
View File

@ -0,0 +1,32 @@
const express = require('express')
const createBundle = require('./util').createBundle
const serveBundle = require('./util').serveBundle
module.exports = createMetamascaraServer
function createMetamascaraServer(){
// start bundlers
const metamascaraBundle = createBundle(__dirname + '/../src/mascara.js')
const proxyBundle = createBundle(__dirname + '/../src/proxy.js')
const uiBundle = createBundle(__dirname + '/../src/ui.js')
const backgroundBuild = createBundle(__dirname + '/../src/background.js')
// serve bundles
const server = express()
// ui window
serveBundle(server, '/ui.js', uiBundle)
server.use(express.static(__dirname+'/../ui/'))
server.use(express.static(__dirname+'/../../dist/chrome'))
// metamascara
serveBundle(server, '/metamascara.js', metamascaraBundle)
// proxy
serveBundle(server, '/proxy/proxy.js', proxyBundle)
server.use('/proxy/', express.static(__dirname+'/../proxy'))
// background
serveBundle(server, '/background.js', backgroundBuild)
return server
}

45
mascara/server/util.js Normal file
View File

@ -0,0 +1,45 @@
const browserify = require('browserify')
const watchify = require('watchify')
module.exports = {
serveBundle,
createBundle,
}
function serveBundle(server, path, bundle){
server.get(path, function(req, res){
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
res.send(bundle.latest)
})
}
function createBundle(entryPoint){
var bundleContainer = {}
var bundler = browserify({
entries: [entryPoint],
cache: {},
packageCache: {},
plugin: [watchify],
})
bundler.on('update', bundle)
bundle()
return bundleContainer
function bundle() {
bundler.bundle(function(err, result){
if (err) {
console.log(`Bundle failed! (${entryPoint})`)
console.error(err)
return
}
console.log(`Bundle updated! (${entryPoint})`)
bundleContainer.latest = result.toString()
})
}
}

155
mascara/src/background.js Normal file
View File

@ -0,0 +1,155 @@
global.window = global
const self = global
const pipe = require('pump')
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
const connectionListener = new SwGlobalListener(self)
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
const PortStream = require('../../app/scripts/lib/port-stream.js')
const DbController = require('idb-global')
const SwPlatform = require('../../app/scripts/platforms/sw')
const MetamaskController = require('../../app/scripts/metamask-controller')
const extension = {} //require('../../app/scripts/lib/extension')
const storeTransform = require('obs-store/lib/transform')
const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = require('../../app/scripts/migrations/')
const firstTimeState = require('../../app/scripts/first-time-state')
const STORAGE_KEY = 'metamask-config'
// const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const METAMASK_DEBUG = true
let popupIsOpen = false
let connectedClientCount = 0
const log = require('loglevel')
global.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting())
})
self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim())
})
console.log('inside:open')
// // state persistence
let diskStore
const dbController = new DbController({
key: STORAGE_KEY,
})
loadStateFromPersistence()
.then((initState) => setupController(initState))
.then(() => console.log('MetaMask initialization complete.'))
.catch((err) => console.error('WHILE SETTING UP:', err))
// initialization flow
//
// State and Persistence
//
function loadStateFromPersistence() {
// migrations
let migrator = new Migrator({ migrations })
const initialState = migrator.generateInitialState(firstTimeState)
dbController.initialState = initialState
return dbController.open()
.then((versionedData) => migrator.migrateData(versionedData))
.then((versionedData) => {
dbController.put(versionedData)
return Promise.resolve(versionedData)
})
.then((versionedData) => Promise.resolve(versionedData.data))
}
function setupController (initState, client) {
//
// MetaMask Controller
//
const platform = new SwPlatform()
const controller = new MetamaskController({
// platform specific implementation
platform,
// User confirmation callbacks:
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnapprovedTx: noop,
// initial state
initState,
})
global.metamaskController = controller
controller.store.subscribe((state) => {
versionifyData(state)
.then((versionedData) => dbController.put(versionedData))
.catch((err) => {console.error(err)})
})
function versionifyData(state) {
return dbController.get()
.then((rawData) => {
return Promise.resolve({
data: state,
meta: rawData.meta,
})}
)
}
//
// connect to other contexts
//
connectionListener.on('remote', (portStream, messageEvent) => {
console.log('REMOTE CONECTION FOUND***********')
connectedClientCount += 1
connectRemote(portStream, messageEvent.data.context)
})
function connectRemote (connectionStream, context) {
var isMetaMaskInternalProcess = (context === 'popup')
if (isMetaMaskInternalProcess) {
// communication with popup
controller.setupTrustedCommunication(connectionStream, 'MetaMask')
popupIsOpen = true
} else {
// communication with page
setupUntrustedCommunication(connectionStream, context)
}
}
function setupUntrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
function setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
}
//
// User Interface setup
//
return Promise.resolve()
}
function sendMessageToAllClients (message) {
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage(message)
})
})
}
function noop () {}

View File

@ -1,19 +1,17 @@
const setupIframe = require('./setup-iframe.js') const setupIframe = require('./setup-iframe.js')
const MetamaskInpageProvider = require('../../app/scripts/lib/inpage-provider.js') const MetamaskInpageProvider = require('../../../app/scripts/lib/inpage-provider.js')
module.exports = getProvider module.exports = getProvider
function getProvider(){ function getProvider(opts){
if (global.web3) { if (global.web3) {
console.log('MetaMask ZeroClient - using environmental web3 provider') console.log('MetaMask ZeroClient - using environmental web3 provider')
return global.web3.currentProvider return global.web3.currentProvider
} }
console.log('MetaMask ZeroClient - injecting zero-client iframe!') console.log('MetaMask ZeroClient - injecting zero-client iframe!')
var iframeStream = setupIframe({ var iframeStream = setupIframe({
zeroClientProvider: 'http://127.0.0.1:9001', zeroClientProvider: opts.mascaraUrl,
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'], sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
container: document.body, container: document.body,
}) })
@ -22,4 +20,3 @@ function getProvider(){
return inpageProvider return inpageProvider
} }

47
mascara/src/mascara.js Normal file
View File

@ -0,0 +1,47 @@
const Web3 = require('web3')
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'
console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN)
//
// setup web3
//
const provider = setupProvider({
mascaraUrl: MASCARA_ORIGIN + '/proxy/',
})
instrumentForUserInteractionTriggers(provider)
const web3 = new Web3(provider)
setupDappAutoReload(web3, provider.publicConfigStore)
//
// ui stuff
//
let shouldPop = false
window.addEventListener('click', maybeTriggerPopup)
//
// util
//
function maybeTriggerPopup(){
if (!shouldPop) return
shouldPop = false
window.open(MASCARA_ORIGIN, '', 'width=360 height=500')
console.log('opening window...')
}
function instrumentForUserInteractionTriggers(provider){
const _super = provider.sendAsync.bind(provider)
provider.sendAsync = function(payload, cb){
if (payload.method === 'eth_sendTransaction') {
console.log('saw send')
shouldPop = true
}
_super(payload, cb)
}
}

26
mascara/src/proxy.js Normal file
View File

@ -0,0 +1,26 @@
const ParentStream = require('iframe-stream').ParentStream
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js')
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
wakeUpInterval: 30000,
intervalDelay,
})
const pageStream = new ParentStream()
background.on('ready', (_) => {
let swStream = SwStream({
serviceWorker: background.controller,
context: 'dapp',
})
pageStream.pipe(swStream).pipe(pageStream)
})
background.on('updatefound', () => window.location.reload())
background.on('error', console.error)
background.startWorker()

56
mascara/src/ui.js Normal file
View File

@ -0,0 +1,56 @@
const injectCss = require('inject-css')
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
const MetaMaskUiCss = require('../../ui/css')
const setupIframe = require('./lib/setup-iframe.js')
const MetamaskInpageProvider = require('../../app/scripts/lib/inpage-provider.js')
const MetamascaraPlatform = require('../../app/scripts/platforms/window')
const startPopup = require('../../app/scripts/popup-core')
// create platform global
global.platform = new MetamascaraPlatform()
var css = MetaMaskUiCss()
injectCss(css)
const container = document.getElementById('app-content')
var name = 'popup'
window.METAMASK_UI_TYPE = name
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
intervalDelay,
wakeUpInterval: 20000
})
// Setup listener for when the service worker is read
const connectApp = function (readSw) {
let connectionStream = SwStream({
serviceWorker: background.controller,
context: name,
})
startPopup({container, connectionStream}, (err, store) => {
if (err) return displayCriticalError(err)
store.subscribe(() => {
const state = store.getState()
if (state.appState.shouldClose) window.close()
})
})
}
background.on('ready', (sw) => {
background.removeListener('updatefound', connectApp)
connectApp(sw)
})
background.on('updatefound', () => window.location.reload())
background.startWorker()
.then(() => {
setTimeout(() => {
const appContent = document.getElementById(`app-content`)
if (!appContent.children.length) window.location.reload()
}, 2000)
})
console.log('hello from MetaMascara ui!')

7
mascara/test/helpers.js Normal file
View File

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

21
mascara/test/index.html Normal file
View File

@ -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>

22
mascara/test/index.js Normal file
View File

@ -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);

4
mascara/test/jquery-3.1.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

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