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

Merge branch 'master' into i#1203MainNetSwitch

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

View File

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

3
.dockerignore Normal file
View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"parserOptions": {
"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
View File

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

View File

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

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

134
README.md
View File

@ -18,11 +18,15 @@ If you're a web dapp developer, we've got two types of guides for you:
Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built.
## 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

View File

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

View File

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

View File

@ -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
@ -78,8 +78,8 @@ function setupController (initState) {
diskStore
)
function versionifyData(state) {
let versionedData = diskStore.getState()
function versionifyData (state) {
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,8 +36,8 @@ class PreferencesController {
}
addToFrequentRpcList (_url) {
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

View File

@ -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,16 +23,19 @@ 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)
// memstore is computed from a few different stores
this._updateMemstore()
this.store.subscribe(() => this._updateMemstore() )
this.networkStore.subscribe(() => this._updateMemstore() )
this.preferencesStore.subscribe(() => this._updateMemstore() )
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')

View File

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

View File

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

View File

@ -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))
@ -581,7 +582,7 @@ class KeyringController extends EventEmitter {
})
}
_updateMemStoreKeyrings() {
_updateMemStoreKeyrings () {
Promise.all(this.keyrings.map(this.displayForKeyring))
.then((keyrings) => {
this.memStore.updateState({ keyrings })

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
const MetamaskConfig = require('../config.js')
const ethUtil = require('ethereumjs-util')
const 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
}
}

View File

@ -10,7 +10,7 @@
const async = require('async')
const EthQuery = require('eth-query')
const ObservableStore = require('obs-store')
function noop() {}
function noop () {}
class EthereumStore extends ObservableStore {
@ -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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ function MetamaskInpageProvider (connectionStream) {
asyncProvider,
(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
@ -125,7 +126,7 @@ function eachJsonMessage (payload, transformFn) {
}
}
function logStreamDisconnectWarning(remoteLabel, err){
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,25 +63,26 @@ 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)
if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb)
async.parallel(reqs, function(err, result) {
async.parallel(reqs, function (err, result) {
if (err) return cb(err)
// write results to txParams obj
Object.assign(txParams, result)
@ -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)}`)
@ -124,14 +123,20 @@ module.exports = class txProviderUtils {
// util
function isUndef(value) {
function isUndef (value) {
return value === undefined
}
function bnToHex(inputBn) {
function bnToHex (inputBn) {
return ethUtil.addHexPrefix(inputBn.toString(16))
}
function hexToBn(inputHex) {
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)
}

View File

@ -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) => {
@ -479,11 +475,11 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
updateAndApproveTx(txMeta, cb) {
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) {
@ -505,7 +501,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
cancelMessage(msgId, cb) {
cancelMessage (msgId, cb) {
const messageManager = this.messageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
@ -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) => {
@ -548,7 +544,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
cancelPersonalMessage(msgId, cb) {
cancelPersonalMessage (msgId, cb) {
const messageManager = this.personalMessageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
@ -562,42 +558,13 @@ 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) {
restoreOldVaultAccounts (migratorOutput) {
const { serialized } = migratorOutput
return this.keyringController.restoreKeyring(serialized)
.then(() => migratorOutput)
}
restoreOldLostAccounts(migratorOutput) {
restoreOldLostAccounts (migratorOutput) {
const { lostAccounts } = migratorOutput
if (lostAccounts) {
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
@ -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()
}
// network
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)
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,36 @@
const version = 12
/*
This migration modifies our notices to delete their body after being read.
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
newState.NoticeController.noticesList.forEach((notice) => {
if (notice.read) {
notice.body = ''
}
})
return newState
}

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,7 @@ module.exports = class NoticeController extends EventEmitter {
var notices = this.getNoticesList()
var 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)

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
const EventEmitter = require('events').EventEmitter
const 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,
})
}

View File

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

View File

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

View File

@ -7,6 +7,6 @@ var changelog = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md')).toSt
var log = changelog.split(version)[1].split('##')[0].trim()
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)

View File

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

View File

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

View File

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

11
docker-compose.yml Normal file
View File

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

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

@ -0,0 +1,14 @@
## Add Custom Build to Chrome
Open `Settings` > `Extensions`.
Check "Developer mode".
At the top, click `Load Unpacked Extension`.
Navigate to your `metamask-plugin/dist/chrome` folder.
Click `Select`.
You now have the plugin, and can click 'inspect views: background plugin' to view its dev console.

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

@ -0,0 +1,14 @@
# Add Custom Build to Firefox
Go to the url `about:debugging`.
Click the button `Load Temporary Add-On`.
Select the file `dist/firefox/manifest.json`.
You can optionally enable debugging, and click `Debug`, for a console window that logs all of Metamask's processes to a single console.
If you have problems debugging, try connecting to the IRC channel `#webextensions` on `irc.mozilla.org`.
For longer questions, use the StackOverfow tag `firefox-addons`.

View File

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

View File

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

View File

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

15
docs/notices.md Normal file
View File

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

19
docs/publishing.md Normal file
View File

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

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

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

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

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

View File

@ -52,6 +52,15 @@ gulp.task('copy:images', copyTask({
'./dist/opera/images',
],
}))
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

View File

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

View File

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

View File

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

View File

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

View File

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

33
mascara/README.md Normal file
View File

@ -0,0 +1,33 @@
start the dual servers (dapp + mascara)
```
npm run mascara
```
### First time use:
- navigate to: http://localhost:9001
- Create an Account
- go back to http://localhost:9002
- open devTools
- click Sync Tx
### Tests:
```
npm run testMascara
```
Test will run in browser, you will have to have these browsers installed:
- Chrome
- Firefox
- Opera
### Deploy:
Will build and deploy mascara via docker
```
docker-compose build && docker-compose stop && docker-compose up -d && docker-compose logs --tail 200 -f
```

View File

@ -1,5 +1,5 @@
window.addEventListener('load', web3Detect)
window.addEventListener('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,

View File

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

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

View File

@ -15,6 +15,6 @@
<body>
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
View File

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

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

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

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

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

View File

@ -1,19 +1,17 @@
const setupIframe = require('./setup-iframe.js')
const 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
View File

@ -0,0 +1,47 @@
const Web3 = require('web3')
const setupProvider = require('./lib/setup-provider.js')
const setupDappAutoReload = require('../../app/scripts/lib/auto-reload.js')
const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001'
console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN)
//
// setup web3
//
const provider = setupProvider({
mascaraUrl: MASCARA_ORIGIN + '/proxy/',
})
instrumentForUserInteractionTriggers(provider)
const web3 = new Web3(provider)
setupDappAutoReload(web3, provider.publicConfigStore)
//
// ui stuff
//
let shouldPop = false
window.addEventListener('click', maybeTriggerPopup)
//
// util
//
function maybeTriggerPopup(){
if (!shouldPop) return
shouldPop = false
window.open(MASCARA_ORIGIN, '', 'width=360 height=500')
console.log('opening window...')
}
function instrumentForUserInteractionTriggers(provider){
const _super = provider.sendAsync.bind(provider)
provider.sendAsync = function(payload, cb){
if (payload.method === 'eth_sendTransaction') {
console.log('saw send')
shouldPop = true
}
_super(payload, cb)
}
}

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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