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:
commit
27220b7bcd
5
.babelrc
5
.babelrc
@ -1 +1,4 @@
|
||||
{ "presets": ["es2015"] }
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["transform-runtime"]
|
||||
}
|
||||
|
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
builds
|
||||
development
|
@ -1 +1,5 @@
|
||||
app/scripts/lib/extension-instance.js
|
||||
test/integration/bundle.js
|
||||
test/integration/jquery-3.1.0.min.js
|
||||
test/integration/helpers.js
|
||||
test/integration/lib/first-time.js
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 6,
|
||||
"ecmaVersion": 2017,
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"impliedStrict": true,
|
||||
@ -17,10 +17,13 @@
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"mocha" : true
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
"mocha",
|
||||
"chai"
|
||||
],
|
||||
|
||||
"globals": {
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -16,3 +16,6 @@ development/bundle.js
|
||||
builds.zip
|
||||
test/integration/bundle.js
|
||||
development/states.js
|
||||
test/background.js
|
||||
test/bundle.js
|
||||
test/test-bundle.js
|
||||
|
115
CHANGELOG.md
115
CHANGELOG.md
@ -3,9 +3,124 @@
|
||||
## Current Master
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
22
Dockerfile
Normal file
22
Dockerfile
Normal 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
134
README.md
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
@ -41,126 +45,22 @@ npm start
|
||||
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
|
||||
|
||||
To write tests that will be run in the browser using QUnit, add your test files to `test/integration/lib`.
|
||||
|
||||
### Deploying the UI
|
||||
## Other Docs
|
||||
|
||||
You must be authorized already on the MetaMask plugin.
|
||||
|
||||
0. Update the version in `app/manifest.json` and the Changelog in `CHANGELOG.md`.
|
||||
1. Visit [the chrome developer dashboard](https://chrome.google.com/webstore/developer/dashboard?authuser=2).
|
||||
2. Run `gulp dist` (or `gulp zip` if you've already built)
|
||||
3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package.
|
||||
- [How to add custom build to Chrome](./docs/add-to-chrome.md)
|
||||
- [How to add custom build to Firefox](./docs/add-to-firefox.md)
|
||||
- [How to develop a live-reloading UI](./docs/ui-dev-mode.md)
|
||||
- [Publishing Guide](./docs/publishing.md)
|
||||
- [How to develop an in-browser mocked UI](./docs/ui-mock-mode.md)
|
||||
- [How to live reload on local dependency changes](./docs/developing-on-deps.md)
|
||||
- [How to add new networks to the Provider Menu](./docs/adding-new-networks.md)
|
||||
- [How to manage notices that appear when the app starts up](./docs/notices.md)
|
||||
- [How to generate a visualization of this repository's development](./docs/development-visualization.md)
|
||||
|
||||
[1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A
|
||||
|
||||
|
||||
### Generate Development Visualization
|
||||
|
||||
This will generate a video of the repo commit history.
|
||||
|
||||
Install preqs:
|
||||
```
|
||||
brew install gource
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
From the repo dir, pipe `gource` into `ffmpeg`:
|
||||
```
|
||||
gource \
|
||||
--seconds-per-day .1 \
|
||||
--user-scale 1.5 \
|
||||
--default-user-image "./images/icon-512.png" \
|
||||
--viewport 1280x720 \
|
||||
--auto-skip-seconds .1 \
|
||||
--multi-sampling \
|
||||
--stop-at-end \
|
||||
--highlight-users \
|
||||
--hide mouse,progress \
|
||||
--file-idle-time 0 \
|
||||
--max-files 0 \
|
||||
--background-colour 000000 \
|
||||
--font-size 18 \
|
||||
--date-format "%b %d, %Y" \
|
||||
--highlight-dirs \
|
||||
--user-friction 0.1 \
|
||||
--title "MetaMask Development History" \
|
||||
--output-ppm-stream - \
|
||||
--output-framerate 30 \
|
||||
| ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4
|
||||
```
|
||||
|
||||
## Generating Notices
|
||||
|
||||
To add a notice:
|
||||
```
|
||||
npm run generateNotice
|
||||
```
|
||||
To delete a notice:
|
||||
```
|
||||
npm run deleteNotice
|
||||
```
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "MetaMask",
|
||||
"short_name": "Metamask",
|
||||
"version": "3.4.0",
|
||||
"version": "3.7.7",
|
||||
"manifest_version": 2,
|
||||
"author": "https://metamask.io",
|
||||
"description": "Ethereum Browser Extension",
|
||||
@ -51,13 +51,14 @@
|
||||
"scripts/contentscript.js"
|
||||
],
|
||||
"run_at": "document_start",
|
||||
"all_frames": false
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"storage",
|
||||
"clipboardWrite",
|
||||
"http://localhost:8545/"
|
||||
"http://localhost:8545/",
|
||||
"https://api.cryptonator.com/"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"scripts/inpage.js"
|
||||
|
@ -1,15 +1,15 @@
|
||||
const urlUtil = require('url')
|
||||
const endOfStream = require('end-of-stream')
|
||||
const asyncQ = require('async-q')
|
||||
const pipe = require('pump')
|
||||
const LocalStorageStore = require('obs-store/lib/localStorage')
|
||||
const storeTransform = require('obs-store/lib/transform')
|
||||
const ExtensionPlatform = require('./platforms/extension')
|
||||
const Migrator = require('./lib/migrator/')
|
||||
const migrations = require('./migrations/')
|
||||
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 extension = require('./lib/extension')
|
||||
const extension = require('extensionizer')
|
||||
const firstTimeState = require('./first-time-state')
|
||||
|
||||
const STORAGE_KEY = 'metamask-config'
|
||||
@ -19,44 +19,42 @@ const log = require('loglevel')
|
||||
window.log = log
|
||||
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
|
||||
|
||||
const platform = new ExtensionPlatform()
|
||||
const notificationManager = new NotificationManager()
|
||||
global.METAMASK_NOTIFIER = notificationManager
|
||||
|
||||
let popupIsOpen = false
|
||||
|
||||
// state persistence
|
||||
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
|
||||
|
||||
// initialization flow
|
||||
asyncQ.waterfall([
|
||||
() => loadStateFromPersistence(),
|
||||
(initState) => setupController(initState),
|
||||
])
|
||||
.then(() => console.log('MetaMask initialization complete.'))
|
||||
.catch((err) => { console.error(err) })
|
||||
initialize().catch(console.error)
|
||||
|
||||
async function initialize() {
|
||||
const initState = await loadStateFromPersistence()
|
||||
await setupController(initState)
|
||||
console.log('MetaMask initialization complete.')
|
||||
}
|
||||
|
||||
//
|
||||
// State and Persistence
|
||||
//
|
||||
|
||||
function loadStateFromPersistence() {
|
||||
async function loadStateFromPersistence () {
|
||||
// migrations
|
||||
let migrator = new Migrator({ migrations })
|
||||
let initialState = migrator.generateInitialState(firstTimeState)
|
||||
return asyncQ.waterfall([
|
||||
const migrator = new Migrator({ migrations })
|
||||
// read from disk
|
||||
() => Promise.resolve(diskStore.getState() || initialState),
|
||||
let versionedData = diskStore.getState() || migrator.generateInitialState(firstTimeState)
|
||||
// migrate data
|
||||
(versionedData) => migrator.migrateData(versionedData),
|
||||
versionedData = await migrator.migrateData(versionedData)
|
||||
// write to disk
|
||||
(versionedData) => {
|
||||
diskStore.putState(versionedData)
|
||||
return Promise.resolve(versionedData)
|
||||
},
|
||||
// resolve to just data
|
||||
(versionedData) => Promise.resolve(versionedData.data),
|
||||
])
|
||||
// return just the data
|
||||
return versionedData.data
|
||||
}
|
||||
|
||||
function setupController (initState) {
|
||||
|
||||
//
|
||||
// MetaMask Controller
|
||||
//
|
||||
@ -68,6 +66,8 @@ function setupController (initState) {
|
||||
showUnapprovedTx: triggerUi,
|
||||
// initial state
|
||||
initState,
|
||||
// platform specific api
|
||||
platform,
|
||||
})
|
||||
global.metamaskController = controller
|
||||
|
||||
@ -79,7 +79,7 @@ function setupController (initState) {
|
||||
)
|
||||
|
||||
function versionifyData (state) {
|
||||
let versionedData = diskStore.getState()
|
||||
const versionedData = diskStore.getState()
|
||||
versionedData.data = state
|
||||
return versionedData
|
||||
}
|
||||
@ -114,13 +114,13 @@ function setupController (initState) {
|
||||
//
|
||||
|
||||
updateBadge()
|
||||
controller.txManager.on('updateBadge', updateBadge)
|
||||
controller.txController.on('updateBadge', updateBadge)
|
||||
controller.messageManager.on('updateBadge', updateBadge)
|
||||
|
||||
// plugin badge text
|
||||
function updateBadge () {
|
||||
var label = ''
|
||||
var unapprovedTxCount = controller.txManager.unapprovedTxCount
|
||||
var unapprovedTxCount = controller.txController.unapprovedTxCount
|
||||
var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
|
||||
var count = unapprovedTxCount + unapprovedMsgCount
|
||||
if (count) {
|
||||
@ -131,7 +131,6 @@ function setupController (initState) {
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
@ -140,7 +139,7 @@ function setupController (initState) {
|
||||
|
||||
// popup trigger
|
||||
function triggerUi () {
|
||||
if (!popupIsOpen) notification.show()
|
||||
if (!popupIsOpen) notificationManager.showPopup()
|
||||
}
|
||||
|
||||
// On first install, open a window to MetaMask website to how-it-works.
|
||||
|
@ -1,14 +1,15 @@
|
||||
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
|
||||
const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask'
|
||||
const DEFAULT_RPC_URL = TESTNET_RPC_URL
|
||||
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
|
||||
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
|
||||
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
|
||||
|
||||
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||
|
||||
module.exports = {
|
||||
network: {
|
||||
default: DEFAULT_RPC_URL,
|
||||
mainnet: MAINET_RPC_URL,
|
||||
testnet: TESTNET_RPC_URL,
|
||||
morden: TESTNET_RPC_URL,
|
||||
ropsten: ROPSTEN_RPC_URL,
|
||||
kovan: KOVAN_RPC_URL,
|
||||
rinkeby: RINKEBY_RPC_URL,
|
||||
},
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ const LocalMessageDuplexStream = require('post-message-stream')
|
||||
const PongStream = require('ping-pong-stream/pong')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const ObjectMultiplex = require('./lib/obj-multiplex')
|
||||
const extension = require('./lib/extension')
|
||||
const extension = require('extensionizer')
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
@ -61,14 +61,22 @@ function setupStreams () {
|
||||
// ignore unused channels (handled by background)
|
||||
mx.ignoreStream('provider')
|
||||
mx.ignoreStream('publicConfig')
|
||||
mx.ignoreStream('reload')
|
||||
}
|
||||
|
||||
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 currentUrl = window.location.href
|
||||
var currentRegex
|
||||
|
@ -39,11 +39,11 @@ class AddressBookController {
|
||||
// pushed object is an object of two fields. Current behavior does not set an
|
||||
// upper limit to the number of addresses.
|
||||
_addToAddressBook (address, name) {
|
||||
let addressBook = this._getAddressBook()
|
||||
let identities = this._getIdentities()
|
||||
const addressBook = this._getAddressBook()
|
||||
const identities = this._getIdentities()
|
||||
|
||||
let 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 addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name })
|
||||
const identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() })
|
||||
// trigger this condition if we own this address--no need to overwrite.
|
||||
if (identitiesIndex !== -1) {
|
||||
return Promise.resolve(addressBook)
|
||||
|
@ -45,15 +45,17 @@ class CurrencyController {
|
||||
|
||||
updateConversionRate () {
|
||||
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((parsedResponse) => {
|
||||
this.setConversionRate(Number(parsedResponse.ticker.price))
|
||||
this.setConversionDate(Number(parsedResponse.timestamp))
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
console.warn('MetaMask - Failed to query currency conversion.')
|
||||
this.setConversionRate(0)
|
||||
this.setConversionDate('N/A')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
129
app/scripts/controllers/network.js
Normal file
129
app/scripts/controllers/network.js
Normal 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)
|
||||
}
|
||||
}
|
@ -36,8 +36,8 @@ class PreferencesController {
|
||||
}
|
||||
|
||||
addToFrequentRpcList (_url) {
|
||||
let rpcList = this.getFrequentRpcList()
|
||||
let index = rpcList.findIndex((element) => { return element === _url })
|
||||
const rpcList = this.getFrequentRpcList()
|
||||
const index = rpcList.findIndex((element) => { return element === _url })
|
||||
if (index !== -1) {
|
||||
rpcList.splice(index, 1)
|
||||
}
|
||||
@ -53,13 +53,9 @@ class PreferencesController {
|
||||
getFrequentRpcList () {
|
||||
return this.store.getState().frequentRpcList
|
||||
}
|
||||
|
||||
//
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = PreferencesController
|
||||
|
@ -4,11 +4,14 @@ const extend = require('xtend')
|
||||
const Semaphore = require('semaphore')
|
||||
const ObservableStore = require('obs-store')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const BN = require('ethereumjs-util').BN
|
||||
const TxProviderUtil = require('./lib/tx-utils')
|
||||
const createId = require('./lib/random-id')
|
||||
const TxProviderUtil = require('../lib/tx-utils')
|
||||
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) {
|
||||
super()
|
||||
this.store = new ObservableStore(extend({
|
||||
@ -20,7 +23,8 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
this.txHistoryLimit = opts.txHistoryLimit
|
||||
this.provider = opts.provider
|
||||
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.signEthTx = opts.signTransaction
|
||||
this.nonceLock = Semaphore(1)
|
||||
@ -30,6 +34,8 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
this.store.subscribe(() => this._updateMemstore())
|
||||
this.networkStore.subscribe(() => this._updateMemstore())
|
||||
this.preferencesStore.subscribe(() => this._updateMemstore())
|
||||
|
||||
this.continuallyResubmitPendingTxs()
|
||||
}
|
||||
|
||||
getState () {
|
||||
@ -37,7 +43,7 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
getNetwork () {
|
||||
return this.networkStore.getState().network
|
||||
return this.networkStore.getState()
|
||||
}
|
||||
|
||||
getSelectedAddress () {
|
||||
@ -46,28 +52,41 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
|
||||
// Returns the tx list
|
||||
getTxList () {
|
||||
let network = this.getNetwork()
|
||||
let fullTxList = this.store.getState().transactions
|
||||
const network = this.getNetwork()
|
||||
const fullTxList = this.getFullTxList()
|
||||
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
|
||||
addTx (txMeta) {
|
||||
var txList = this.getTxList()
|
||||
var txHistoryLimit = this.txHistoryLimit
|
||||
const txCount = this.getTxCount()
|
||||
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
|
||||
// and then if it is removes only confirmed
|
||||
// or rejected tx's.
|
||||
// not tx's that are pending or unapproved
|
||||
if (txList.length > txHistoryLimit - 1) {
|
||||
var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
|
||||
txList.splice(index, 1)
|
||||
if (txCount > txHistoryLimit - 1) {
|
||||
var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
|
||||
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.removeAllListeners(`${txMeta.id}:rejected`)
|
||||
})
|
||||
@ -89,7 +108,7 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
//
|
||||
updateTx (txMeta) {
|
||||
var txId = txMeta.id
|
||||
var txList = this.getTxList()
|
||||
var txList = this.getFullTxList()
|
||||
var index = txList.findIndex(txData => txData.id === txId)
|
||||
txList[index] = txMeta
|
||||
this._saveTxList(txList)
|
||||
@ -109,44 +128,38 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
async.waterfall([
|
||||
// validate
|
||||
(cb) => this.txProviderUtils.validateTxParams(txParams, cb),
|
||||
// prepare txMeta
|
||||
// construct txMeta
|
||||
(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 = {
|
||||
id: txId,
|
||||
time: time,
|
||||
id: createId(),
|
||||
time: (new Date()).getTime(),
|
||||
status: 'unapproved',
|
||||
metamaskNetworkId: this.getNetwork(),
|
||||
txParams: txParams,
|
||||
}
|
||||
// calculate metadata for tx
|
||||
this.txProviderUtils.analyzeGasUsage(txMeta, cb)
|
||||
cb()
|
||||
},
|
||||
// add default tx params
|
||||
(cb) => this.addTxDefaults(txMeta, cb),
|
||||
// save txMeta
|
||||
(cb) => {
|
||||
this.addTx(txMeta)
|
||||
this.setMaxTxCostAndFee(txMeta)
|
||||
cb(null, txMeta)
|
||||
},
|
||||
], done)
|
||||
}
|
||||
|
||||
setMaxTxCostAndFee (txMeta) {
|
||||
var txParams = txMeta.txParams
|
||||
var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16)
|
||||
var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16)
|
||||
var txFee = gasCost.mul(gasPrice)
|
||||
var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
|
||||
var maxCost = txValue.add(txFee)
|
||||
txMeta.txFee = txFee
|
||||
txMeta.txValue = txValue
|
||||
txMeta.maxCost = maxCost
|
||||
txMeta.gasPrice = gasPrice
|
||||
this.updateTx(txMeta)
|
||||
addTxDefaults (txMeta, cb) {
|
||||
const txParams = txMeta.txParams
|
||||
// ensure value
|
||||
txParams.value = txParams.value || '0x0'
|
||||
this.query.gasPrice((err, gasPrice) => {
|
||||
if (err) return cb(err)
|
||||
// set gasPrice
|
||||
txParams.gasPrice = gasPrice
|
||||
// set gasLimit
|
||||
this.txProviderUtils.analyzeGasUsage(txMeta, cb)
|
||||
})
|
||||
}
|
||||
|
||||
getUnapprovedTxList () {
|
||||
@ -172,7 +185,10 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
], (err) => {
|
||||
self.nonceLock.leave()
|
||||
if (err) {
|
||||
this.setTxStatusFailed(txId)
|
||||
this.setTxStatusFailed(txId, {
|
||||
errCode: err.errCode || err,
|
||||
message: err.message || 'Transaction failed during approval',
|
||||
})
|
||||
return cb(err)
|
||||
}
|
||||
cb()
|
||||
@ -186,7 +202,7 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
fillInTxParams (txId, cb) {
|
||||
let txMeta = this.getTx(txId)
|
||||
const txMeta = this.getTx(txId)
|
||||
this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => {
|
||||
if (err) return cb(err)
|
||||
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) {
|
||||
let txMeta = this.getTx(txId)
|
||||
let txParams = txMeta.txParams
|
||||
let fromAddress = txParams.from
|
||||
let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams)
|
||||
const txMeta = this.getTx(txId)
|
||||
const txParams = txMeta.txParams
|
||||
const fromAddress = txParams.from
|
||||
// add network/chain id
|
||||
txParams.chainId = this.getChainId()
|
||||
const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams)
|
||||
this.signEthTx(ethTx, fromAddress).then(() => {
|
||||
this.setTxStatusSigned(txMeta.id)
|
||||
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) => {
|
||||
if (err) return cb(err)
|
||||
this.setTxHash(txId, txHash)
|
||||
@ -219,7 +251,7 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
// receives a txHash records the tx as signed
|
||||
setTxHash (txId, txHash) {
|
||||
// Add the tx hash to the persisted meta-tx object
|
||||
let txMeta = this.getTx(txId)
|
||||
const txMeta = this.getTx(txId)
|
||||
txMeta.hash = txHash
|
||||
this.updateTx(txMeta)
|
||||
}
|
||||
@ -291,7 +323,10 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
this._setTxStatus(txId, 'confirmed')
|
||||
}
|
||||
|
||||
setTxStatusFailed (txId) {
|
||||
setTxStatusFailed (txId, reason) {
|
||||
const txMeta = this.getTx(txId)
|
||||
txMeta.err = reason
|
||||
this.updateTx(txMeta)
|
||||
this._setTxStatus(txId, 'failed')
|
||||
}
|
||||
|
||||
@ -312,14 +347,13 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
var txHash = txMeta.hash
|
||||
var txId = txMeta.id
|
||||
if (!txHash) {
|
||||
txMeta.err = {
|
||||
const errReason = {
|
||||
errCode: 'No hash was provided',
|
||||
message: 'We had an error while submitting this transaction, please try again.',
|
||||
}
|
||||
this.updateTx(txMeta)
|
||||
return this.setTxStatusFailed(txId)
|
||||
return this.setTxStatusFailed(txId, errReason)
|
||||
}
|
||||
this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => {
|
||||
this.query.getTransactionByHash(txHash, (err, txParams) => {
|
||||
if (err || !txParams) {
|
||||
if (!txParams) return
|
||||
txMeta.err = {
|
||||
@ -328,7 +362,7 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
message: 'There was a problem loading this transaction.',
|
||||
}
|
||||
this.updateTx(txMeta)
|
||||
return console.error(err)
|
||||
return log.error(err)
|
||||
}
|
||||
if (txParams.blockNumber) {
|
||||
this.setTxStatusConfirmed(txId)
|
||||
@ -354,6 +388,7 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
this.emit(`${txMeta.id}:${status}`, txId)
|
||||
if (status === 'submitted' || status === 'rejected') {
|
||||
this.emit(`${txMeta.id}:finished`, txMeta)
|
||||
|
||||
}
|
||||
this.updateTx(txMeta)
|
||||
this.emit('updateBadge')
|
||||
@ -373,7 +408,47 @@ module.exports = class TransactionManager extends EventEmitter {
|
||||
})
|
||||
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')
|
@ -6,9 +6,11 @@ const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||
// The default state of MetaMask
|
||||
//
|
||||
module.exports = {
|
||||
config: {
|
||||
config: {},
|
||||
NetworkController: {
|
||||
provider: {
|
||||
type: (METAMASK_DEBUG || env === 'test') ? 'testnet' : 'mainnet',
|
||||
type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet',
|
||||
type: 'rinkeby',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -31,26 +31,11 @@ web3.setProvider = function () {
|
||||
console.log('MetaMask - overrode web3.setProvider')
|
||||
}
|
||||
console.log('MetaMask - injected web3')
|
||||
// export global web3, with usage-detection reload fn
|
||||
var triggerReload = setupDappAutoReload(web3)
|
||||
|
||||
// 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)
|
||||
// export global web3, with usage-detection
|
||||
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
|
||||
|
||||
// set web3 defaultAccount
|
||||
|
||||
inpageProvider.publicConfigStore.subscribe(function (state) {
|
||||
web3.eth.defaultAccount = state.selectedAddress
|
||||
})
|
||||
|
@ -187,7 +187,7 @@ class KeyringController extends EventEmitter {
|
||||
.then((accounts) => {
|
||||
switch (type) {
|
||||
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'))
|
||||
default:
|
||||
return Promise.resolve(newAccount)
|
||||
@ -324,6 +324,7 @@ class KeyringController extends EventEmitter {
|
||||
if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
|
||||
const hexAccount = normalizeAddress(firstAccount)
|
||||
this.emit('newAccount', hexAccount)
|
||||
this.emit('newVault', hexAccount)
|
||||
return this.setupAccounts(accounts)
|
||||
})
|
||||
.then(this.persistAllKeyrings.bind(this))
|
||||
|
@ -3,10 +3,18 @@ const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||
const env = process.env.METAMASK_ENV
|
||||
|
||||
module.exports = function (address) {
|
||||
if (METAMASK_DEBUG || env === 'test') return // Don't faucet in development or test
|
||||
var http = new XMLHttpRequest()
|
||||
var data = address
|
||||
http.open('POST', uri, true)
|
||||
http.setRequestHeader('Content-type', 'application/rawdata')
|
||||
http.send(data)
|
||||
// Don't faucet in development or test
|
||||
if (METAMASK_DEBUG === true || env === 'test') return
|
||||
global.log.info('auto-fauceting:', address)
|
||||
const data = address
|
||||
const headers = new Headers()
|
||||
headers.append('Content-type', 'application/rawdata')
|
||||
fetch(uri, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: data,
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
@ -1,30 +1,33 @@
|
||||
const once = require('once')
|
||||
const ensnare = require('ensnare')
|
||||
|
||||
module.exports = setupDappAutoReload
|
||||
|
||||
function setupDappAutoReload (web3) {
|
||||
function setupDappAutoReload (web3, observable) {
|
||||
// export web3 as a global, checking for usage
|
||||
var pageIsUsingWeb3 = false
|
||||
var resetWasRequested = false
|
||||
global.web3 = ensnare(web3, once(function () {
|
||||
// if web3 usage happened after a reset request, trigger reset late
|
||||
if (resetWasRequested) return triggerReset()
|
||||
// mark web3 as used
|
||||
pageIsUsingWeb3 = true
|
||||
// reset web3 reference
|
||||
global.web3 = web3
|
||||
}))
|
||||
global.web3 = new Proxy(web3, {
|
||||
get: (_web3, name) => {
|
||||
// get the time of use
|
||||
if (name !== '_used') _web3._used = Date.now()
|
||||
return _web3[name]
|
||||
},
|
||||
set: (_web3, name, value) => {
|
||||
_web3[name] = value
|
||||
},
|
||||
})
|
||||
var networkVersion
|
||||
|
||||
return handleResetRequest
|
||||
observable.subscribe(function (state) {
|
||||
// get the initial network
|
||||
const curentNetVersion = state.networkVersion
|
||||
if (!networkVersion) networkVersion = curentNetVersion
|
||||
|
||||
function handleResetRequest () {
|
||||
resetWasRequested = true
|
||||
// ignore if web3 was not used
|
||||
if (!pageIsUsingWeb3) return
|
||||
// reload after short timeout
|
||||
setTimeout(triggerReset, 500)
|
||||
if (curentNetVersion !== networkVersion && web3._used) {
|
||||
const timeSinceUse = Date.now() - web3._used
|
||||
// if web3 was recently used then delay the reloading of the page
|
||||
timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500)
|
||||
// prevent reentry into if statement if state updates again before
|
||||
// reload
|
||||
networkVersion = curentNetVersion
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// reload the page
|
||||
|
23
app/scripts/lib/buy-eth-url.js
Normal file
23
app/scripts/lib/buy-eth-url.js
Normal 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
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
const MetamaskConfig = require('../config.js')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
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 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
|
||||
* wrapping a pojo-migrator.
|
||||
@ -32,36 +34,6 @@ ConfigManager.prototype.getConfig = function () {
|
||||
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) {
|
||||
this.store.putState(data)
|
||||
}
|
||||
@ -135,6 +107,35 @@ ConfigManager.prototype.getSeedWords = function () {
|
||||
var data = this.getData()
|
||||
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 () {
|
||||
var provider = this.getProvider()
|
||||
@ -144,14 +145,17 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
|
||||
case 'mainnet':
|
||||
return MAINNET_RPC
|
||||
|
||||
case 'testnet':
|
||||
return TESTNET_RPC
|
||||
case 'ropsten':
|
||||
return ROPSTEN_RPC
|
||||
|
||||
case 'morden':
|
||||
return MORDEN_RPC
|
||||
case 'kovan':
|
||||
return KOVAN_RPC
|
||||
|
||||
case 'rinkeby':
|
||||
return RINKEBY_RPC
|
||||
|
||||
default:
|
||||
return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC
|
||||
return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ class EthereumStore extends ObservableStore {
|
||||
super({
|
||||
accounts: {},
|
||||
transactions: {},
|
||||
currentBlockNumber: '0',
|
||||
currentBlockHash: '',
|
||||
currentBlockGasLimit: '',
|
||||
})
|
||||
this._provider = opts.provider
|
||||
this._query = new EthQuery(this._provider)
|
||||
@ -69,6 +72,9 @@ class EthereumStore extends ObservableStore {
|
||||
_updateForBlock (block) {
|
||||
const blockNumber = '0x' + block.number.toString('hex')
|
||||
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([
|
||||
this._updateAccounts.bind(this),
|
||||
this._updateTransactions.bind(this, blockNumber),
|
||||
|
@ -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
|
@ -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()
|
7
app/scripts/lib/hex-to-bn.js
Normal file
7
app/scripts/lib/hex-to-bn.js
Normal 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)
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
@ -34,6 +34,7 @@ function MetamaskInpageProvider (connectionStream) {
|
||||
asyncProvider,
|
||||
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
|
||||
)
|
||||
// start and stop polling to unblock first block lock
|
||||
|
||||
self.idMap = {}
|
||||
// handle sendAsync requests via asyncProvider
|
||||
@ -85,7 +86,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
|
||||
break
|
||||
|
||||
case 'net_version':
|
||||
let networkVersion = self.publicConfigStore.getState().networkVersion
|
||||
const networkVersion = self.publicConfigStore.getState().networkVersion
|
||||
result = networkVersion
|
||||
break
|
||||
|
||||
|
@ -1,42 +1,35 @@
|
||||
const asyncQ = require('async-q')
|
||||
|
||||
class Migrator {
|
||||
|
||||
constructor (opts = {}) {
|
||||
let migrations = opts.migrations || []
|
||||
const migrations = opts.migrations || []
|
||||
// sort migrations by 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
|
||||
this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0
|
||||
}
|
||||
|
||||
// run all pending migrations on meta in place
|
||||
migrateData (versionedData = this.generateInitialState()) {
|
||||
let remaining = this.migrations.filter(migrationIsPending)
|
||||
async migrateData (versionedData = this.generateInitialState()) {
|
||||
const pendingMigrations = this.migrations.filter(migrationIsPending)
|
||||
|
||||
return (
|
||||
asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration))
|
||||
.then(() => versionedData)
|
||||
)
|
||||
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')
|
||||
}
|
||||
|
||||
// migration is "pending" if hit has a higher
|
||||
return versionedData
|
||||
|
||||
// migration is "pending" if it has a higher
|
||||
// version number than currentVersion
|
||||
function migrationIsPending (migration) {
|
||||
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) {
|
||||
return {
|
||||
meta: {
|
||||
|
71
app/scripts/lib/notification-manager.js
Normal file
71
app/scripts/lib/notification-manager.js
Normal 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
|
@ -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)
|
||||
})
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
const async = require('async')
|
||||
const EthQuery = require('eth-query')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const Transaction = require('ethereumjs-tx')
|
||||
const normalize = require('eth-sig-util').normalize
|
||||
@ -7,53 +6,55 @@ const BN = ethUtil.BN
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
module.exports = class txProviderUtils {
|
||||
constructor (provider) {
|
||||
this.provider = provider
|
||||
this.query = new EthQuery(provider)
|
||||
|
||||
constructor (ethQuery) {
|
||||
this.query = ethQuery
|
||||
}
|
||||
|
||||
analyzeGasUsage (txData, cb) {
|
||||
analyzeGasUsage (txMeta, cb) {
|
||||
var self = this
|
||||
this.query.getBlockByNumber('latest', true, (err, block) => {
|
||||
if (err) return cb(err)
|
||||
async.waterfall([
|
||||
self.estimateTxGas.bind(self, txData, block.gasLimit),
|
||||
self.setTxGas.bind(self, txData, block.gasLimit),
|
||||
self.estimateTxGas.bind(self, txMeta, block.gasLimit),
|
||||
self.setTxGas.bind(self, txMeta, block.gasLimit),
|
||||
], cb)
|
||||
})
|
||||
}
|
||||
|
||||
estimateTxGas (txData, blockGasLimitHex, cb) {
|
||||
const txParams = txData.txParams
|
||||
estimateTxGas (txMeta, blockGasLimitHex, cb) {
|
||||
const txParams = txMeta.txParams
|
||||
// check if gasLimit is already specified
|
||||
txData.gasLimitSpecified = Boolean(txParams.gas)
|
||||
txMeta.gasLimitSpecified = Boolean(txParams.gas)
|
||||
// if not, fallback to block gasLimit
|
||||
if (!txData.gasLimitSpecified) {
|
||||
txParams.gas = blockGasLimitHex
|
||||
if (!txMeta.gasLimitSpecified) {
|
||||
const blockGasLimitBN = hexToBn(blockGasLimitHex)
|
||||
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
|
||||
txParams.gas = bnToHex(saferGasLimitBN)
|
||||
}
|
||||
// run tx, see if it will OOG
|
||||
this.query.estimateGas(txParams, cb)
|
||||
}
|
||||
|
||||
setTxGas (txData, blockGasLimitHex, estimatedGasHex, cb) {
|
||||
txData.estimatedGas = estimatedGasHex
|
||||
const txParams = txData.txParams
|
||||
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex, cb) {
|
||||
txMeta.estimatedGas = estimatedGasHex
|
||||
const txParams = txMeta.txParams
|
||||
|
||||
// if gasLimit was specified and doesnt OOG,
|
||||
// use original specified amount
|
||||
if (txData.gasLimitSpecified) {
|
||||
txData.estimatedGas = txParams.gas
|
||||
if (txMeta.gasLimitSpecified) {
|
||||
txMeta.estimatedGas = txParams.gas
|
||||
cb()
|
||||
return
|
||||
}
|
||||
// if gasLimit not originally specified,
|
||||
// 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
|
||||
cb()
|
||||
return
|
||||
@ -62,19 +63,20 @@ module.exports = class txProviderUtils {
|
||||
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
|
||||
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
||||
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
||||
const upperGasLimitBn = blockGasLimitBn.muln(0.9)
|
||||
const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)
|
||||
|
||||
// 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 (bufferedGasLimitBn.lt(blockGasLimitBn)) return bnToHex(bufferedGasLimitBn)
|
||||
if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn)
|
||||
// otherwise use blockGasLimit
|
||||
return bnToHex(blockGasLimitBn)
|
||||
return bnToHex(upperGasLimitBn)
|
||||
}
|
||||
|
||||
fillInTxParams (txParams, cb) {
|
||||
let fromAddress = txParams.from
|
||||
let reqs = {}
|
||||
const fromAddress = txParams.from
|
||||
const reqs = {}
|
||||
|
||||
if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb)
|
||||
if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb)
|
||||
@ -90,16 +92,13 @@ module.exports = class txProviderUtils {
|
||||
|
||||
// builds ethTx from txParams object
|
||||
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
|
||||
txParams.to = normalize(txParams.to)
|
||||
txParams.from = normalize(txParams.from)
|
||||
txParams.value = normalize(txParams.value)
|
||||
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)
|
||||
// build ethTx
|
||||
log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`)
|
||||
@ -135,3 +134,9 @@ function bnToHex(inputBn) {
|
||||
function hexToBn (inputHex) {
|
||||
return new BN(ethUtil.stripHexPrefix(inputHex), 16)
|
||||
}
|
||||
|
||||
function BnMultiplyByFraction (targetBN, numerator, denominator) {
|
||||
const numBN = new BN(numerator)
|
||||
const denomBN = new BN(denominator)
|
||||
return targetBN.mul(numBN).div(denomBN)
|
||||
}
|
||||
|
@ -4,13 +4,12 @@ const promiseToCallback = require('promise-to-callback')
|
||||
const pipe = require('pump')
|
||||
const Dnode = require('dnode')
|
||||
const ObservableStore = require('obs-store')
|
||||
const storeTransform = require('obs-store/lib/transform')
|
||||
const EthStore = require('./lib/eth-store')
|
||||
const EthQuery = require('eth-query')
|
||||
const streamIntoProvider = require('web3-stream-provider/handler')
|
||||
const MetaMaskProvider = require('web3-provider-engine/zero.js')
|
||||
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
||||
const KeyringController = require('./keyring-controller')
|
||||
const NetworkController = require('./controllers/network')
|
||||
const PreferencesController = require('./controllers/preferences')
|
||||
const CurrencyController = require('./controllers/currency')
|
||||
const NoticeController = require('./notice-controller')
|
||||
@ -18,13 +17,12 @@ const ShapeShiftController = require('./controllers/shapeshift')
|
||||
const AddressBookController = require('./controllers/address-book')
|
||||
const MessageManager = require('./lib/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 extension = require('./lib/extension')
|
||||
const autoFaucet = require('./lib/auto-faucet')
|
||||
const nodeify = require('./lib/nodeify')
|
||||
const IdStoreMigrator = require('./lib/idStore-migrator')
|
||||
const accountImporter = require('./account-import-strategies')
|
||||
const getBuyEthUrl = require('./lib/buy-eth-url')
|
||||
|
||||
const version = require('../manifest.json').version
|
||||
|
||||
@ -33,14 +31,17 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
constructor (opts) {
|
||||
super()
|
||||
this.opts = opts
|
||||
let initState = opts.initState || {}
|
||||
const initState = opts.initState || {}
|
||||
|
||||
// platform-specific api
|
||||
this.platform = opts.platform
|
||||
|
||||
// observable state store
|
||||
this.store = new ObservableStore(initState)
|
||||
|
||||
// network store
|
||||
this.networkStore = new ObservableStore({ network: 'loading' })
|
||||
|
||||
this.networkController = new NetworkController(initState.NetworkController)
|
||||
// config manager
|
||||
this.configManager = new ConfigManager({
|
||||
store: this.store,
|
||||
@ -60,8 +61,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
// rpc provider
|
||||
this.provider = this.initializeProvider()
|
||||
this.provider.on('block', this.logBlock.bind(this))
|
||||
this.provider.on('error', this.verifyNetwork.bind(this))
|
||||
|
||||
// eth data query tools
|
||||
this.ethQuery = new EthQuery(this.provider)
|
||||
@ -74,10 +73,12 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
this.keyringController = new KeyringController({
|
||||
initState: initState.KeyringController,
|
||||
ethStore: this.ethStore,
|
||||
getNetwork: this.getNetworkState.bind(this),
|
||||
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
|
||||
})
|
||||
this.keyringController.on('newAccount', (address) => {
|
||||
this.preferencesController.setSelectedAddress(address)
|
||||
})
|
||||
this.keyringController.on('newVault', (address) => {
|
||||
autoFaucet(address)
|
||||
})
|
||||
|
||||
@ -87,15 +88,16 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
}, this.keyringController)
|
||||
|
||||
// tx mgmt
|
||||
this.txManager = new TxManager({
|
||||
initState: initState.TransactionManager,
|
||||
networkStore: this.networkStore,
|
||||
this.txController = new TransactionController({
|
||||
initState: initState.TransactionController || initState.TransactionManager,
|
||||
networkStore: this.networkController.networkStore,
|
||||
preferencesStore: this.preferencesController.store,
|
||||
txHistoryLimit: 40,
|
||||
getNetwork: this.getNetworkState.bind(this),
|
||||
getNetwork: this.networkController.getNetworkState.bind(this),
|
||||
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
||||
provider: this.provider,
|
||||
blockTracker: this.provider,
|
||||
ethQuery: this.ethQuery,
|
||||
})
|
||||
|
||||
// notices
|
||||
@ -110,19 +112,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
initState: initState.ShapeShiftController,
|
||||
})
|
||||
|
||||
this.lookupNetwork()
|
||||
this.networkController.lookupNetwork()
|
||||
this.messageManager = new MessageManager()
|
||||
this.personalMessageManager = new PersonalMessageManager()
|
||||
this.publicConfigStore = this.initPublicConfigStore()
|
||||
|
||||
// TEMPORARY UNTIL FULL DEPRECATION:
|
||||
this.idStoreMigrator = new IdStoreMigrator({
|
||||
configManager: this.configManager,
|
||||
})
|
||||
|
||||
// manual disk state subscriptions
|
||||
this.txManager.store.subscribe((state) => {
|
||||
this.store.updateState({ TransactionManager: state })
|
||||
this.txController.store.subscribe((state) => {
|
||||
this.store.updateState({ TransactionController: state })
|
||||
})
|
||||
this.keyringController.store.subscribe((state) => {
|
||||
this.store.updateState({ KeyringController: state })
|
||||
@ -142,11 +139,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
this.shapeshiftController.store.subscribe((state) => {
|
||||
this.store.updateState({ ShapeShiftController: state })
|
||||
})
|
||||
this.networkController.store.subscribe((state) => {
|
||||
this.store.updateState({ NetworkController: state })
|
||||
})
|
||||
|
||||
// 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.txManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.txController.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.personalMessageManager.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 () {
|
||||
|
||||
let provider = MetaMaskProvider({
|
||||
return this.networkController.initializeProvider({
|
||||
static: {
|
||||
eth_syncing: false,
|
||||
web3_clientVersion: `MetaMask/v${version}`,
|
||||
},
|
||||
rpcUrl: this.configManager.getCurrentRpcAddress(),
|
||||
rpcUrl: this.networkController.getCurrentRpcAddress(),
|
||||
// account mgmt
|
||||
getAccounts: (cb) => {
|
||||
let selectedAddress = this.preferencesController.getSelectedAddress()
|
||||
let result = selectedAddress ? [selectedAddress] : []
|
||||
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
|
||||
const result = []
|
||||
const selectedAddress = this.preferencesController.getSelectedAddress()
|
||||
// only show address if account is unlocked
|
||||
if (isUnlocked && selectedAddress) {
|
||||
result.push(selectedAddress)
|
||||
}
|
||||
cb(null, result)
|
||||
},
|
||||
// tx signing
|
||||
@ -183,26 +187,23 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
// new style msg signing
|
||||
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
|
||||
})
|
||||
return provider
|
||||
}
|
||||
|
||||
initPublicConfigStore () {
|
||||
// get init state
|
||||
const publicConfigStore = new ObservableStore()
|
||||
|
||||
// sync publicConfigStore with transform
|
||||
pipe(
|
||||
this.store,
|
||||
storeTransform(selectPublicState.bind(this)),
|
||||
publicConfigStore
|
||||
)
|
||||
// memStore -> transform -> publicConfigStore
|
||||
this.on('update', (memState) => {
|
||||
const publicState = selectPublicState(memState)
|
||||
publicConfigStore.putState(publicState)
|
||||
})
|
||||
|
||||
function selectPublicState(state) {
|
||||
const result = { selectedAddress: undefined }
|
||||
try {
|
||||
result.selectedAddress = state.PreferencesController.selectedAddress
|
||||
result.networkVersion = this.getNetworkState()
|
||||
} catch (_) {}
|
||||
function selectPublicState (memState) {
|
||||
const result = {
|
||||
selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined,
|
||||
networkVersion: memState.network,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -214,7 +215,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
//
|
||||
|
||||
getState () {
|
||||
|
||||
const wallet = this.configManager.getWallet()
|
||||
const vault = this.keyringController.store.getState().vault
|
||||
const isInitialized = (!!wallet || !!vault)
|
||||
@ -222,9 +222,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
{
|
||||
isInitialized,
|
||||
},
|
||||
this.networkStore.getState(),
|
||||
this.networkController.store.getState(),
|
||||
this.ethStore.getState(),
|
||||
this.txManager.memStore.getState(),
|
||||
this.txController.memStore.getState(),
|
||||
this.messageManager.memStore.getState(),
|
||||
this.personalMessageManager.memStore.getState(),
|
||||
this.keyringController.memStore.getState(),
|
||||
@ -249,15 +249,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
getApi () {
|
||||
const keyringController = this.keyringController
|
||||
const preferencesController = this.preferencesController
|
||||
const txManager = this.txManager
|
||||
const txController = this.txController
|
||||
const noticeController = this.noticeController
|
||||
const addressBookController = this.addressBookController
|
||||
|
||||
return {
|
||||
// etc
|
||||
getState: (cb) => cb(null, this.getState()),
|
||||
setProviderType: this.setProviderType.bind(this),
|
||||
useEtherscanProvider: this.useEtherscanProvider.bind(this),
|
||||
setProviderType: this.networkController.setProviderType.bind(this.networkController),
|
||||
setCurrentCurrency: this.setCurrentCurrency.bind(this),
|
||||
markAccountsFound: this.markAccountsFound.bind(this),
|
||||
// coinbase
|
||||
@ -290,9 +289,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
|
||||
exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
|
||||
|
||||
// txManager
|
||||
approveTransaction: txManager.approveTransaction.bind(txManager),
|
||||
cancelTransaction: txManager.cancelTransaction.bind(txManager),
|
||||
// txController
|
||||
approveTransaction: txController.approveTransaction.bind(txController),
|
||||
cancelTransaction: txController.cancelTransaction.bind(txController),
|
||||
updateAndApproveTransaction: this.updateAndApproveTx.bind(this),
|
||||
|
||||
// messageManager
|
||||
@ -344,9 +343,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
console.error('Error in RPC response:\n', response.error)
|
||||
}
|
||||
if (request.isMetamaskInternal) return
|
||||
if (global.METAMASK_DEBUG) {
|
||||
console.log(`RPC (${originDomain}):`, request, '->', response)
|
||||
}
|
||||
log.info(`RPC (${originDomain}):`, request, '->', response)
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,8 +363,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
//
|
||||
|
||||
submitPassword (password, cb) {
|
||||
this.migrateOldVaultIfAny(password)
|
||||
.then(this.keyringController.submitPassword.bind(this.keyringController, password))
|
||||
return this.keyringController.submitPassword(password)
|
||||
.then((newState) => { cb(null, newState) })
|
||||
.catch((reason) => { cb(reason) })
|
||||
}
|
||||
@ -393,7 +389,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
.then((serialized) => {
|
||||
const seedWords = serialized.mnemonic
|
||||
this.configManager.setSeedWords(seedWords)
|
||||
cb()
|
||||
cb(null, seedWords)
|
||||
})
|
||||
}
|
||||
|
||||
@ -425,12 +421,12 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
newUnapprovedTransaction (txParams, cb) {
|
||||
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
|
||||
const self = this
|
||||
self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => {
|
||||
self.txController.addUnapprovedTransaction(txParams, (err, txMeta) => {
|
||||
if (err) return cb(err)
|
||||
self.sendUpdate()
|
||||
self.opts.showUnapprovedTx(txMeta)
|
||||
// listen for tx completion (success, fail)
|
||||
self.txManager.once(`${txMeta.id}:finished`, (completedTx) => {
|
||||
self.txController.once(`${txMeta.id}:finished`, (completedTx) => {
|
||||
switch (completedTx.status) {
|
||||
case 'submitted':
|
||||
return cb(null, completedTx.hash)
|
||||
@ -444,7 +440,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
}
|
||||
|
||||
newUnsignedMessage (msgParams, cb) {
|
||||
let msgId = this.messageManager.addUnapprovedMessage(msgParams)
|
||||
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
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.'))
|
||||
}
|
||||
|
||||
let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
|
||||
const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
this.personalMessageManager.once(`${msgId}:finished`, (data) => {
|
||||
@ -481,9 +477,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
updateAndApproveTx (txMeta, cb) {
|
||||
log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`)
|
||||
const txManager = this.txManager
|
||||
txManager.updateTx(txMeta)
|
||||
txManager.approveTransaction(txMeta.id, cb)
|
||||
const txController = this.txController
|
||||
txController.updateTx(txMeta)
|
||||
txController.approveTransaction(txMeta.id, cb)
|
||||
}
|
||||
|
||||
signMessage (msgParams, cb) {
|
||||
@ -515,7 +511,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
// Prefixed Style Message Signing Methods:
|
||||
approvePersonalMessage (msgParams, cb) {
|
||||
let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
|
||||
const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
|
||||
this.sendUpdate()
|
||||
this.opts.showUnconfirmedMessage()
|
||||
this.personalMessageManager.once(`${msgId}:finished`, (data) => {
|
||||
@ -562,35 +558,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
cb(null, this.getState())
|
||||
}
|
||||
|
||||
// Migrate Old Vault If Any
|
||||
// @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
|
||||
return this.keyringController.restoreKeyring(serialized)
|
||||
@ -623,12 +590,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
//
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
@ -647,91 +608,29 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
buyEth (address, amount) {
|
||||
if (!amount) amount = '5'
|
||||
|
||||
const network = this.getNetworkState()
|
||||
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
|
||||
}
|
||||
|
||||
if (url) extension.tabs.create({ url })
|
||||
const network = this.networkController.getNetworkState()
|
||||
const url = getBuyEthUrl({ network, address, amount })
|
||||
if (url) this.platform.openWindow({ url })
|
||||
}
|
||||
|
||||
createShapeShiftTx (depositAddress, depositType) {
|
||||
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
|
||||
}
|
||||
|
||||
//
|
||||
// network
|
||||
//
|
||||
|
||||
verifyNetwork () {
|
||||
// Check network when restoring connectivity:
|
||||
if (this.isNetworkLoading()) this.lookupNetwork()
|
||||
}
|
||||
|
||||
setDefaultRpc () {
|
||||
this.configManager.setRpcTarget('http://localhost:8545')
|
||||
extension.runtime.reload()
|
||||
this.lookupNetwork()
|
||||
this.networkController.setRpcTarget('http://localhost:8545')
|
||||
return Promise.resolve('http://localhost:8545')
|
||||
}
|
||||
|
||||
setCustomRpc (rpcTarget, rpcList) {
|
||||
this.configManager.setRpcTarget(rpcTarget)
|
||||
this.networkController.setRpcTarget(rpcTarget)
|
||||
|
||||
return this.preferencesController.updateFrequentRpcList(rpcTarget)
|
||||
.then(() => {
|
||||
extension.runtime.reload()
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
if (versionedData.data.config.provider.type === 'etherscan') {
|
||||
|
@ -8,7 +8,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
if (versionedData.data.config.provider.rpcTarget === oldTestRpc) {
|
||||
|
@ -6,7 +6,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (versionedData) {
|
||||
let safeVersionedData = clone(versionedData)
|
||||
const safeVersionedData = clone(versionedData)
|
||||
safeVersionedData.meta.version = version
|
||||
try {
|
||||
if (safeVersionedData.data.config.provider.type !== 'rpc') return Promise.resolve(safeVersionedData)
|
||||
|
@ -14,7 +14,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
@ -12,7 +12,7 @@ module.exports = {
|
||||
version,
|
||||
|
||||
migrate: function (originalVersionedData) {
|
||||
let versionedData = clone(originalVersionedData)
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
try {
|
||||
const state = versionedData.data
|
||||
|
36
app/scripts/migrations/012.js
Normal file
36
app/scripts/migrations/012.js
Normal 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
|
||||
}
|
34
app/scripts/migrations/013.js
Normal file
34
app/scripts/migrations/013.js
Normal 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
|
||||
}
|
34
app/scripts/migrations/014.js
Normal file
34
app/scripts/migrations/014.js
Normal 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
|
||||
}
|
@ -20,10 +20,10 @@ module.exports = {
|
||||
migrate: function (versionedData) {
|
||||
versionedData.meta.version = version
|
||||
|
||||
let store = new ObservableStore(versionedData.data)
|
||||
let configManager = new ConfigManager({ store })
|
||||
let idStoreMigrator = new IdentityStoreMigrator({ configManager })
|
||||
let keyringController = new KeyringController({
|
||||
const store = new ObservableStore(versionedData.data)
|
||||
const configManager = new ConfigManager({ store })
|
||||
const idStoreMigrator = new IdentityStoreMigrator({ configManager })
|
||||
const keyringController = new KeyringController({
|
||||
configManager: configManager,
|
||||
})
|
||||
|
||||
@ -46,6 +46,5 @@ module.exports = {
|
||||
return Promise.resolve(versionedData)
|
||||
})
|
||||
})
|
||||
|
||||
},
|
||||
}
|
||||
|
@ -22,4 +22,7 @@ module.exports = [
|
||||
require('./009'),
|
||||
require('./010'),
|
||||
require('./011'),
|
||||
require('./012'),
|
||||
require('./013'),
|
||||
require('./014'),
|
||||
]
|
||||
|
@ -41,6 +41,7 @@ module.exports = class NoticeController extends EventEmitter {
|
||||
var notices = this.getNoticesList()
|
||||
var index = notices.findIndex((currentNotice) => currentNotice.id === noticeToMark.id)
|
||||
notices[index].read = true
|
||||
notices[index].body = ''
|
||||
this.setNoticesList(notices)
|
||||
const latestNotice = this.getLatestUnreadNotice()
|
||||
cb(null, latestNotice)
|
||||
|
23
app/scripts/platforms/extension.js
Normal file
23
app/scripts/platforms/extension.js
Normal 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
|
24
app/scripts/platforms/sw.js
Normal file
24
app/scripts/platforms/sw.js
Normal 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
|
22
app/scripts/platforms/window.js
Normal file
22
app/scripts/platforms/window.js
Normal 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
|
@ -1,7 +1,8 @@
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const async = require('async')
|
||||
const Dnode = require('dnode')
|
||||
const Web3 = require('web3')
|
||||
const MetaMaskUi = require('../../ui')
|
||||
const EthQuery = require('eth-query')
|
||||
const launchMetamaskUi = require('../../ui')
|
||||
const StreamProvider = require('web3-stream-provider')
|
||||
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
||||
|
||||
@ -9,9 +10,12 @@ const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
||||
module.exports = initializePopup
|
||||
|
||||
|
||||
function initializePopup (connectionStream) {
|
||||
function initializePopup ({ container, connectionStream }, cb) {
|
||||
// setup app
|
||||
connectToAccountManager(connectionStream, setupApp)
|
||||
async.waterfall([
|
||||
(cb) => connectToAccountManager(connectionStream, cb),
|
||||
(accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb),
|
||||
], cb)
|
||||
}
|
||||
|
||||
function connectToAccountManager (connectionStream, cb) {
|
||||
@ -28,7 +32,8 @@ function setupWeb3Connection (connectionStream) {
|
||||
providerStream.pipe(connectionStream).pipe(providerStream)
|
||||
connectionStream.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) {
|
||||
@ -47,19 +52,3 @@ function setupControllerConnection (connectionStream, cb) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
@ -3,23 +3,47 @@ const MetaMaskUiCss = require('../../ui/css')
|
||||
const startPopup = require('./popup-core')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const isPopupOrNotification = require('./lib/is-popup-or-notification')
|
||||
const extension = require('./lib/extension')
|
||||
const notification = require('./lib/notifications')
|
||||
const extension = require('extensionizer')
|
||||
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)
|
||||
|
||||
var name = isPopupOrNotification()
|
||||
closePopupIfOpen(name)
|
||||
window.METAMASK_UI_TYPE = name
|
||||
// identify window type (popup, notification)
|
||||
const windowType = isPopupOrNotification()
|
||||
global.METAMASK_UI_TYPE = windowType
|
||||
closePopupIfOpen(windowType)
|
||||
|
||||
var pluginPort = extension.runtime.connect({ name })
|
||||
var portStream = new PortStream(pluginPort)
|
||||
// setup stream to background
|
||||
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') {
|
||||
notification.closePopup()
|
||||
|
||||
function closePopupIfOpen (windowType) {
|
||||
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
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
machine:
|
||||
node:
|
||||
version: 6.0.0
|
||||
version: 7.6.0
|
||||
dependencies:
|
||||
pre:
|
||||
- "npm i -g testem"
|
||||
|
@ -7,6 +7,6 @@ var changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md')).toSt
|
||||
|
||||
var log = changelog.split(version)[1].split('##')[0].trim()
|
||||
|
||||
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)
|
||||
|
@ -4,7 +4,7 @@
|
||||
* 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 apis = [
|
||||
|
70
development/states/private-key-export-success.json
Normal file
70
development/states/private-key-export-success.json
Normal 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": {}
|
||||
}
|
69
development/states/private-key-export.json
Normal file
69
development/states/private-key-export.json
Normal 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
11
docker-compose.yml
Normal 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
14
docs/add-to-chrome.md
Normal 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
14
docs/add-to-firef.md
Normal 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`.
|
||||
|
25
docs/adding-new-networks.md
Normal file
25
docs/adding-new-networks.md
Normal 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
|
||||
|
10
docs/developing-on-deps.md
Normal file
10
docs/developing-on-deps.md
Normal 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!
|
||||
|
35
docs/development-visualization.md
Normal file
35
docs/development-visualization.md
Normal 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
15
docs/notices.md
Normal 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
19
docs/publishing.md
Normal 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
6
docs/ui-dev-mode.md
Normal 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
8
docs/ui-mock-mode.md
Normal 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.
|
||||
|
14
gulpfile.js
14
gulpfile.js
@ -52,6 +52,15 @@ gulp.task('copy:images', copyTask({
|
||||
'./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({
|
||||
source: './app/fonts/',
|
||||
destinations: [
|
||||
@ -127,6 +136,7 @@ const staticFiles = [
|
||||
]
|
||||
|
||||
var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`)
|
||||
copyStrings.push('copy:contractImages')
|
||||
|
||||
if (!disableLiveReload) {
|
||||
copyStrings.push('copy:reload')
|
||||
@ -182,7 +192,7 @@ gulp.task('build:js', gulp.parallel(...jsBuildStrings))
|
||||
// disc bundle analyzer tasks
|
||||
|
||||
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}`)))
|
||||
@ -296,8 +306,6 @@ function bundleTask(opts) {
|
||||
return (
|
||||
|
||||
bundler.bundle()
|
||||
// log errors if they happen
|
||||
.on('error', gutil.log.bind(gutil, 'Browserify Error'))
|
||||
// convert bundle stream to gulp vinyl stream
|
||||
.pipe(source(opts.filename))
|
||||
// inject variables into bundle
|
||||
|
@ -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)
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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
33
mascara/README.md
Normal 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
|
||||
```
|
@ -1,5 +1,5 @@
|
||||
|
||||
window.addEventListener('load', web3Detect)
|
||||
window.addEventListener('message', console.warn)
|
||||
|
||||
function web3Detect() {
|
||||
if (global.web3) {
|
||||
@ -13,10 +13,10 @@ function web3Detect() {
|
||||
function startApp(){
|
||||
console.log('app started')
|
||||
|
||||
var primaryAccount = null
|
||||
var primaryAccount
|
||||
console.log('getting main account...')
|
||||
web3.eth.getAccounts(function(err, addresses){
|
||||
if (err) throw err
|
||||
web3.eth.getAccounts((err, addresses) => {
|
||||
if (err) console.error(err)
|
||||
console.log('set address', addresses[0])
|
||||
primaryAccount = addresses[0]
|
||||
})
|
||||
@ -24,6 +24,7 @@ function startApp(){
|
||||
document.querySelector('.action-button-1').addEventListener('click', function(){
|
||||
console.log('saw click')
|
||||
console.log('sending tx')
|
||||
primaryAccount
|
||||
web3.eth.sendTransaction({
|
||||
from: primaryAccount,
|
||||
to: primaryAccount,
|
@ -3,15 +3,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>MetaMask ZeroClient Example</title>
|
||||
|
||||
<script src="http://localhost:9001/metamascara.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button class="action-button-1">SYNC TX</button>
|
||||
<button class="action-button-2">ASYNC TX</button>
|
||||
<script src="./zero.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
31
mascara/example/server.js
Normal file
31
mascara/example/server.js
Normal 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}`)
|
@ -15,6 +15,6 @@
|
||||
|
||||
<body>
|
||||
Hello! I am the MetaMask iframe.
|
||||
<script src="/controller.js"></script>
|
||||
<script src="./proxy.js"></script>
|
||||
</body>
|
||||
</html>
|
32
mascara/server/index.js
Normal file
32
mascara/server/index.js
Normal 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
45
mascara/server/util.js
Normal 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
155
mascara/src/background.js
Normal 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 () {}
|
@ -1,19 +1,17 @@
|
||||
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
|
||||
|
||||
|
||||
function getProvider(){
|
||||
|
||||
function getProvider(opts){
|
||||
if (global.web3) {
|
||||
console.log('MetaMask ZeroClient - using environmental web3 provider')
|
||||
return global.web3.currentProvider
|
||||
}
|
||||
|
||||
console.log('MetaMask ZeroClient - injecting zero-client iframe!')
|
||||
var iframeStream = setupIframe({
|
||||
zeroClientProvider: 'http://127.0.0.1:9001',
|
||||
zeroClientProvider: opts.mascaraUrl,
|
||||
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
|
||||
container: document.body,
|
||||
})
|
||||
@ -22,4 +20,3 @@ function getProvider(){
|
||||
return inpageProvider
|
||||
|
||||
}
|
||||
|
47
mascara/src/mascara.js
Normal file
47
mascara/src/mascara.js
Normal 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
26
mascara/src/proxy.js
Normal 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
56
mascara/src/ui.js
Normal 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
7
mascara/test/helpers.js
Normal 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
21
mascara/test/index.html
Normal 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
22
mascara/test/index.js
Normal 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
4
mascara/test/jquery-3.1.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
119
mascara/test/lib/first-time.js
Normal file
119
mascara/test/lib/first-time.js
Normal file
@ -0,0 +1,119 @@
|
||||
const PASSWORD = 'password123'
|
||||
|
||||
QUnit.module('first time usage')
|
||||
|
||||
QUnit.test('render init screen', function (assert) {
|
||||
var done = assert.async()
|
||||
let app
|
||||
|
||||
wait(1000).then(function() {
|
||||
app = $('#app-content').contents()
|
||||
const recurseNotices = function () {
|
||||
let button = app.find('button')
|
||||
if (button.html() === 'Accept') {
|
||||
let termsPage = app.find('.markdown')[0]
|
||||
termsPage.scrollTop = termsPage.scrollHeight
|
||||
return wait().then(() => {
|
||||
button.click()
|
||||
return wait()
|
||||
}).then(() => {
|
||||
return recurseNotices()
|
||||
})
|
||||
} else {
|
||||
return wait()
|
||||
}
|
||||
}
|
||||
return recurseNotices()
|
||||
}).then(function() {
|
||||
// Scroll through terms
|
||||
var title = app.find('h1').text()
|
||||
assert.equal(title, 'MetaMask', 'title screen')
|
||||
|
||||
// enter password
|
||||
var pwBox = app.find('#password-box')[0]
|
||||
var confBox = app.find('#password-box-confirm')[0]
|
||||
pwBox.value = PASSWORD
|
||||
confBox.value = PASSWORD
|
||||
|
||||
return wait()
|
||||
}).then(function() {
|
||||
|
||||
// create vault
|
||||
var createButton = app.find('button.primary')[0]
|
||||
createButton.click()
|
||||
|
||||
return wait(1500)
|
||||
}).then(function() {
|
||||
|
||||
var created = app.find('h3')[0]
|
||||
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
|
||||
|
||||
// Agree button
|
||||
var button = app.find('button')[0]
|
||||
assert.ok(button, 'button present')
|
||||
button.click()
|
||||
|
||||
return wait(1000)
|
||||
}).then(function() {
|
||||
|
||||
var detail = app.find('.account-detail-section')[0]
|
||||
assert.ok(detail, 'Account detail section loaded.')
|
||||
|
||||
var sandwich = app.find('.sandwich-expando')[0]
|
||||
sandwich.click()
|
||||
|
||||
return wait()
|
||||
}).then(function() {
|
||||
|
||||
var sandwich = app.find('.menu-droppo')[0]
|
||||
var children = sandwich.children
|
||||
var lock = children[children.length - 2]
|
||||
assert.ok(lock, 'Lock menu item found')
|
||||
lock.click()
|
||||
|
||||
return wait(1000)
|
||||
}).then(function() {
|
||||
|
||||
var pwBox = app.find('#password-box')[0]
|
||||
pwBox.value = PASSWORD
|
||||
|
||||
var createButton = app.find('button.primary')[0]
|
||||
createButton.click()
|
||||
|
||||
return wait(1000)
|
||||
}).then(function() {
|
||||
|
||||
var detail = app.find('.account-detail-section')[0]
|
||||
assert.ok(detail, 'Account detail section loaded again.')
|
||||
|
||||
return wait()
|
||||
}).then(function (){
|
||||
|
||||
var qrButton = app.find('.fa.fa-qrcode')[0]
|
||||
qrButton.click()
|
||||
|
||||
return wait(1000)
|
||||
}).then(function (){
|
||||
|
||||
var qrHeader = app.find('.qr-header')[0]
|
||||
var qrContainer = app.find('#qr-container')[0]
|
||||
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
|
||||
assert.ok(qrContainer, 'QR Container found')
|
||||
|
||||
return wait()
|
||||
}).then(function (){
|
||||
|
||||
var networkMenu = app.find('.network-indicator')[0]
|
||||
networkMenu.click()
|
||||
|
||||
return wait()
|
||||
}).then(function (){
|
||||
|
||||
var networkMenu = app.find('.network-indicator')[0]
|
||||
var children = networkMenu.children
|
||||
children.length[3]
|
||||
assert.ok(children, 'All network options present')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
13
mascara/test/testem.yml
Normal file
13
mascara/test/testem.yml
Normal file
@ -0,0 +1,13 @@
|
||||
launch_in_dev:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- Opera
|
||||
launch_in_ci:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- Opera
|
||||
framework:
|
||||
- qunit
|
||||
before_tests: "npm run mascaraCi"
|
||||
after_tests: "rm ./background.js ./test-bundle.js ./bundle.js"
|
||||
test_page: "./index.html"
|
40
mascara/test/util/mascara-test-helper.js
Normal file
40
mascara/test/util/mascara-test-helper.js
Normal file
@ -0,0 +1,40 @@
|
||||
const EventEmitter = require('events')
|
||||
const IDB = require('idb-global')
|
||||
const KEY = 'metamask-test-config'
|
||||
module.exports = class Helper extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
}
|
||||
|
||||
tryToCleanContext () {
|
||||
this.unregister()
|
||||
.then(() => this.clearDb())
|
||||
.then(() => super.emit('complete'))
|
||||
.catch((err) => super.emit('complete'))
|
||||
}
|
||||
|
||||
unregister () {
|
||||
return global.navigator.serviceWorker.getRegistration()
|
||||
.then((registration) => {
|
||||
if (registration) return registration.unregister()
|
||||
.then((b) => b ? Promise.resolve() : Promise.reject())
|
||||
else return Promise.resolve()
|
||||
})
|
||||
}
|
||||
clearDb () {
|
||||
return new Promise ((resolve, reject) => {
|
||||
const deleteRequest = global.indexDB.deleteDatabase(KEY)
|
||||
deleteRequest.addEventListener('success', resolve)
|
||||
deleteRequest.addEventListener('error', reject)
|
||||
})
|
||||
|
||||
}
|
||||
mockState (state) {
|
||||
const db = new IDB({
|
||||
version: 2,
|
||||
key: KEY,
|
||||
initialState: state
|
||||
})
|
||||
return db.open()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user