Merge branch 'master' into AddTokenList

This commit is contained in:
Dan Finlay 2017-06-12 17:06:39 -07:00
commit a741cc4fc4
160 changed files with 3175 additions and 7504 deletions

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

25
.gitignore vendored
View File

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

View File

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

View File

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

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.5.2",
"version": "3.7.8",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
@ -58,7 +58,7 @@
"storage",
"clipboardWrite",
"http://localhost:8545/",
"https://www.cryptonator.com/"
"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,6 +1,5 @@
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')
@ -30,38 +29,32 @@ let popupIsOpen = false
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([
// read from disk
() => Promise.resolve(diskStore.getState() || initialState),
// migrate data
(versionedData) => migrator.migrateData(versionedData),
// write to disk
(versionedData) => {
diskStore.putState(versionedData)
return Promise.resolve(versionedData)
},
// resolve to just data
(versionedData) => Promise.resolve(versionedData.data),
])
const migrator = new Migrator({ migrations })
// read from disk
let versionedData = diskStore.getState() || migrator.generateInitialState(firstTimeState)
// migrate data
versionedData = await migrator.migrateData(versionedData)
// write to disk
diskStore.putState(versionedData)
// return just the data
return versionedData.data
}
function setupController (initState) {
//
// MetaMask Controller
//
@ -85,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
}
@ -121,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) {
@ -138,7 +131,6 @@ function setupController (initState) {
}
return Promise.resolve()
}
//

View File

@ -1,16 +1,15 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask'
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const DEFAULT_RPC_URL = TESTNET_RPC_URL
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

@ -61,7 +61,6 @@ function setupStreams () {
// ignore unused channels (handled by background)
mx.ignoreStream('provider')
mx.ignoreStream('publicConfig')
mx.ignoreStream('reload')
}
function shouldInjectWeb3 () {
@ -77,7 +76,7 @@ function doctypeCheck () {
}
}
function suffixCheck() {
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) => {
console.warn('MetaMask - Failed to query currency conversion.')
this.setConversionRate(0)
this.setConversionDate('N/A')
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 EthQuery = require('eth-query')
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,17 +23,19 @@ module.exports = class TransactionManager extends EventEmitter {
this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider
this.blockTracker = opts.blockTracker
this.query = new EthQuery(this.provider)
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 () {
@ -38,7 +43,7 @@ module.exports = class TransactionManager extends EventEmitter {
}
getNetwork () {
return this.networkStore.getState().network
return this.networkStore.getState()
}
getSelectedAddress () {
@ -47,8 +52,8 @@ module.exports = class TransactionManager extends EventEmitter {
// Returns the tx list
getTxList () {
let network = this.getNetwork()
let fullTxList = this.getFullTxList()
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network)
}
@ -64,10 +69,10 @@ module.exports = class TransactionManager extends EventEmitter {
// Adds a tx to the txlist
addTx (txMeta) {
let txCount = this.getTxCount()
let network = this.getNetwork()
let fullTxList = this.getFullTxList()
let txHistoryLimit = this.txHistoryLimit
const txCount = this.getTxCount()
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
const txHistoryLimit = this.txHistoryLimit
// checks if the length of the tx history is
// longer then desired persistence limit
@ -197,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)
@ -205,7 +210,7 @@ module.exports = class TransactionManager extends EventEmitter {
})
}
getChainId() {
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState.network)
if (Number.isNaN(getChainId)) {
@ -230,7 +235,11 @@ module.exports = class TransactionManager extends EventEmitter {
})
}
publishTransaction (txId, rawTx, cb) {
publishTransaction (txId, rawTx, cb = warn) {
const txMeta = this.getTx(txId)
txMeta.rawTx = rawTx
this.updateTx(txMeta)
this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => {
if (err) return cb(err)
this.setTxHash(txId, txHash)
@ -242,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)
}
@ -315,7 +324,7 @@ module.exports = class TransactionManager extends EventEmitter {
}
setTxStatusFailed (txId, reason) {
let txMeta = this.getTx(txId)
const txMeta = this.getTx(txId)
txMeta.err = reason
this.updateTx(txMeta)
this._setTxStatus(txId, 'failed')
@ -338,7 +347,7 @@ module.exports = class TransactionManager extends EventEmitter {
var txHash = txMeta.hash
var txId = txMeta.id
if (!txHash) {
let errReason = {
const errReason = {
errCode: 'No hash was provided',
message: 'We had an error while submitting this transaction, please try again.',
}
@ -353,7 +362,7 @@ module.exports = class TransactionManager extends EventEmitter {
message: 'There was a problem loading this transaction.',
}
this.updateTx(txMeta)
return console.error(err)
return log.error(err)
}
if (txParams.blockNumber) {
this.setTxStatusConfirmed(txId)
@ -380,6 +389,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')
@ -399,7 +409,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

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

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

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

@ -1,6 +1,6 @@
module.exports = getBuyEthUrl
function getBuyEthUrl({ network, amount, address }){
function getBuyEthUrl ({ network, amount, address }) {
let url
switch (network) {
case '1':
@ -11,9 +11,13 @@ function getBuyEthUrl({ network, amount, address }){
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,11 +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.
@ -33,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)
}
@ -136,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()
@ -145,17 +145,17 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
case 'mainnet':
return MAINNET_RPC
case 'testnet':
return TESTNET_RPC
case 'morden':
return MORDEN_RPC
case 'ropsten':
return ROPSTEN_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 {
@ -21,6 +21,7 @@ class EthereumStore extends ObservableStore {
transactions: {},
currentBlockNumber: '0',
currentBlockHash: '',
currentBlockGasLimit: '',
})
this._provider = opts.provider
this._query = new EthQuery(this._provider)
@ -73,6 +74,7 @@ class EthereumStore extends ObservableStore {
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

@ -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)
return (
asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration))
.then(() => versionedData)
)
async migrateData (versionedData = this.generateInitialState()) {
const pendingMigrations = this.migrations.filter(migrationIsPending)
// migration is "pending" if hit has a higher
for (const index in pendingMigrations) {
const migration = pendingMigrations[index]
versionedData = await migration.migrate(versionedData)
if (!versionedData.data) throw new Error('Migrator - migration returned empty data')
if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
}
return versionedData
// migration is "pending" if it has a higher
// version number than currentVersion
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

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

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,15 +6,14 @@ 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 (txMeta, cb) {
@ -35,7 +33,9 @@ module.exports = class txProviderUtils {
txMeta.gasLimitSpecified = Boolean(txParams.gas)
// if not, fallback to block gasLimit
if (!txMeta.gasLimitSpecified) {
txParams.gas = blockGasLimitHex
const blockGasLimitBN = hexToBn(blockGasLimitHex)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
txParams.gas = bnToHex(saferGasLimitBN)
}
// run tx, see if it will OOG
this.query.estimateGas(txParams, cb)
@ -75,14 +75,14 @@ module.exports = class txProviderUtils {
}
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)
@ -123,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,7 +17,7 @@ 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 autoFaucet = require('./lib/auto-faucet')
const nodeify = require('./lib/nodeify')
@ -32,7 +31,7 @@ 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
@ -41,8 +40,8 @@ module.exports = class MetamaskController extends EventEmitter {
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,
@ -62,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)
@ -76,7 +73,7 @@ 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)
@ -91,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
@ -114,14 +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()
// 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 })
@ -141,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))
@ -161,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
@ -182,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
}
@ -220,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(),
@ -247,62 +249,61 @@ 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),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
getState: (cb) => cb(null, this.getState()),
setProviderType: this.networkController.setProviderType.bind(this.networkController),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
// coinbase
buyEth: this.buyEth.bind(this),
// shapeshift
createShapeShiftTx: this.createShapeShiftTx.bind(this),
// primary HD keyring management
addNewAccount: this.addNewAccount.bind(this),
placeSeedWords: this.placeSeedWords.bind(this),
clearSeedWordCache: this.clearSeedWordCache.bind(this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
addNewAccount: this.addNewAccount.bind(this),
placeSeedWords: this.placeSeedWords.bind(this),
clearSeedWordCache: this.clearSeedWordCache.bind(this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
// vault management
submitPassword: this.submitPassword.bind(this),
// PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this),
setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc).bind(this),
setCustomRpc: nodeify(this.setCustomRpc).bind(this),
// AddressController
setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController),
setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController),
// KeyringController
setLocked: nodeify(keyringController.setLocked).bind(keyringController),
setLocked: nodeify(keyringController.setLocked).bind(keyringController),
createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController),
addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController),
addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
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
signMessage: nodeify(this.signMessage).bind(this),
cancelMessage: this.cancelMessage.bind(this),
signMessage: nodeify(this.signMessage).bind(this),
cancelMessage: this.cancelMessage.bind(this),
// personalMessageManager
signPersonalMessage: nodeify(this.signPersonalMessage).bind(this),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this),
signPersonalMessage: nodeify(this.signPersonalMessage).bind(this),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this),
// notices
checkNotices: noticeController.updateNoticesList.bind(noticeController),
checkNotices: noticeController.updateNoticesList.bind(noticeController),
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
}
}
@ -342,9 +343,7 @@ module.exports = class MetamaskController extends EventEmitter {
console.error('Error in RPC response:\n', response.error)
}
if (request.isMetamaskInternal) return
if (global.METAMASK_DEBUG) {
console.log(`RPC (${originDomain}):`, request, '->', response)
}
log.info(`RPC (${originDomain}):`, request, '->', response)
}
}
@ -422,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)
@ -441,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) => {
@ -461,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) => {
@ -476,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) {
@ -502,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') {
@ -512,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) => {
@ -545,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') {
@ -559,13 +558,13 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, this.getState())
}
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))
@ -591,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 {
@ -615,7 +608,7 @@ module.exports = class MetamaskController extends EventEmitter {
buyEth (address, amount) {
if (!amount) amount = '5'
const network = this.getNetworkState()
const network = this.networkController.getNetworkState()
const url = getBuyEthUrl({ network, address, amount })
if (url) this.platform.openWindow({ url })
}
@ -623,71 +616,21 @@ module.exports = class MetamaskController extends EventEmitter {
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')
this.platform.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(() => {
this.platform.reload()
this.lookupNetwork()
return Promise.resolve(rpcTarget)
})
}
setProviderType (type) {
this.configManager.setProviderType(type)
this.platform.reload()
this.lookupNetwork()
}
useEtherscanProvider () {
this.configManager.useEtherscanProvider()
this.platform.reload()
}
getNetworkState () {
return this.networkStore.getState().network
}
setNetworkState (network) {
return this.networkStore.updateState({ network })
}
isNetworkLoading () {
return this.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)
.then(() => {
return Promise.resolve(rpcTarget)
})
}
}

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

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

View File

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

View File

@ -15,15 +15,15 @@ const KeyringController = require('../../app/scripts/lib/keyring-controller')
const password = 'obviously not correct'
module.exports = {
version,
version,
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

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

View File

@ -1,7 +1,7 @@
const EventEmitter = require('events').EventEmitter
const async = require('async')
const Dnode = require('dnode')
const Web3 = require('web3')
const EthQuery = require('eth-query')
const launchMetamaskUi = require('../../ui')
const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
@ -16,7 +16,6 @@ function initializePopup ({ container, connectionStream }, cb) {
(cb) => connectToAccountManager(connectionStream, cb),
(accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb),
], cb)
}
function connectToAccountManager (connectionStream, cb) {
@ -33,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) {

View File

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

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)

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,20 +1,33 @@
start the dual servers (dapp + mascara)
```
node server.js
npm run mascara
```
## First time use:
### First time use:
- navigate to: http://localhost:9001/popup/popup.html
- navigate to: http://localhost:9001
- Create an Account
- go back to http://localhost:9002/
- go back to http://localhost:9002
- open devTools
- click Sync Tx
### Todos
### Tests:
- [ ] Figure out user flows and UI redesign
- [ ] Figure out FireFox
Standing problems:
- [ ] IndexDb
```
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,4 +1,5 @@
global.window = global
const self = global
const pipe = require('pump')
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
@ -6,7 +7,7 @@ const connectionListener = new SwGlobalListener(self)
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
const PortStream = require('../../app/scripts/lib/port-stream.js')
const DbController = require('./lib/index-db-controller')
const DbController = require('idb-global')
const SwPlatform = require('../../app/scripts/platforms/sw')
const MetamaskController = require('../../app/scripts/metamask-controller')
@ -21,6 +22,7 @@ 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
@ -40,7 +42,6 @@ console.log('inside:open')
let diskStore
const dbController = new DbController({
key: STORAGE_KEY,
version: 2,
})
loadStateFromPersistence()
.then((initState) => setupController(initState))
@ -107,6 +108,7 @@ function setupController (initState, client) {
connectionListener.on('remote', (portStream, messageEvent) => {
console.log('REMOTE CONECTION FOUND***********')
connectedClientCount += 1
connectRemote(portStream, messageEvent.data.context)
})
@ -142,4 +144,12 @@ function setupController (initState, client) {
return Promise.resolve()
}
function sendMessageToAllClients (message) {
self.clients.matchAll().then(function(clients) {
clients.forEach(function(client) {
client.postMessage(message)
})
})
}
function noop () {}

View File

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

View File

@ -1,6 +1,6 @@
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)
@ -14,8 +14,7 @@ const provider = setupProvider({
instrumentForUserInteractionTriggers(provider)
const web3 = new Web3(provider)
global.web3 = web3
setupDappAutoReload(web3, provider.publicConfigStore)
//
// ui stuff
//

View File

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

View File

@ -24,10 +24,10 @@ const background = new SWcontroller({
fileName: '/background.js',
letBeIdle: false,
intervalDelay,
wakeUpInterval: 30000
wakeUpInterval: 20000
})
// Setup listener for when the service worker is read
background.on('ready', (readSw) => {
const connectApp = function (readSw) {
let connectionStream = SwStream({
serviceWorker: background.controller,
context: name,
@ -39,6 +39,18 @@ background.on('ready', (readSw) => {
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

View File

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

13
mascara/test/testem.yml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
2
3

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

18
test/lib/mock-store.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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