diff --git a/.babelrc b/.babelrc index 9d8d51656..307583ffd 100644 --- a/.babelrc +++ b/.babelrc @@ -1 +1,4 @@ -{ "presets": ["es2015"] } +{ + "presets": ["es2015", "stage-0", "react"], + "plugins": ["transform-runtime", "transform-async-to-generator"] +} diff --git a/.eslintignore b/.eslintignore index df49525be..e4cade21c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,6 @@ 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 +ui/lib/blockies.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 8bbfe13c7..20a2a7a00 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,8 @@ { + "parser": "babel-eslint", "parserOptions": { "sourceType": "module", - "ecmaVersion": 6, + "ecmaVersion": 2017, "ecmaFeatures": { "experimentalObjectRestSpread": true, "impliedStrict": true, @@ -10,17 +11,25 @@ "arrowFunctions": true, "objectLiteralShorthandMethods": true, "objectLiteralShorthandProperties": true, - "templateStrings": true + "templateStrings": true, + "classes": true, + "jsx": true }, }, + "extends": ["plugin:react/recommended"], + "env": { "es6": true, "node": true, - "browser": true + "browser": true, + "mocha" : true }, "plugins": [ + "mocha", + "chai", + "react" ], "globals": { @@ -47,8 +56,8 @@ "eqeqeq": [2, "allow-null"], "generator-star-spacing": [2, { "before": true, "after": true }], "handle-callback-err": [1, "^(err|error)$" ], - "indent": [2, 2, { "SwitchCase": 1 }], - "jsx-quotes": [2, "prefer-single"], + "indent": "off", + "jsx-quotes": [2, "prefer-double"], "key-spacing": 1, "keyword-spacing": [2, { "before": true, "after": true }], "new-cap": [2, { "newIsCap": true, "capIsNew": false }], @@ -130,7 +139,7 @@ "no-with": 2, "one-var": [2, { "initialized": "never" }], "operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], - "padded-blocks": [1, "never"], + "padded-blocks": "off", "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], "semi": [2, "never"], "semi-spacing": [2, { "before": false, "after": true }], diff --git a/.gitignore b/.gitignore index c61847aab..92b3f2875 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,35 @@ -dist npm-debug.log node_modules -temp -.tmp -.sass-cache +package-lock.json + app/bower_components test/bower_components package + +.idea + +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 + +#ignore css output and sourcemaps +ui/app/css/output/ + +notes.txt + +.coveralls.yml +.nyc_output \ No newline at end of file diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 000000000..854829a54 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,10 @@ +app/ +development/ +dist/ +docs/ +fonts/ +images/ +mascara/ +node_modules/ +notices/ +test/ diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 000000000..d080d68d9 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,50 @@ +{ + "extends": "stylelint-config-standard", + "rules": { + "color-named": "never", + "font-family-name-quotes": "always-where-recommended", + "font-weight-notation": "numeric", + "function-url-quotes": "always", + "number-leading-zero": "never", + "value-no-vendor-prefix": true, + "value-list-comma-newline-before": "never-multi-line", + "custom-property-empty-line-before": "never", + "property-no-unknown": [ + true, + { + "ignoreProperties": [ + "composes", + "all", + "-webkit-appearance" + ] + } + ], + "declaration-block-semicolon-newline-after": "always", + "block-opening-brace-newline-after": "always", + "selector-attribute-quotes": "always", + "selector-max-specificity": "0,5,2", + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["local", "global"] + } + ], + "at-rule-empty-line-before": [ + "always", + { + "ignore": [ + "after-comment", + ] + } + ], + "indentation": [ + 2, + { + "indentInsideParens": "once-at-root-twice-in-block" + } + ], + "max-nesting-depth": 3, + "no-duplicate-selectors": true, + "no-unknown-animations": true + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index fd754cadd..abc89f9c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,405 @@ ## Current Master +## 4.1.0 2018-2-27 + +- Report failed txs to Sentry with more specific message +- Fix internal feature flags being sometimes undefined +- Standardized license to MIT + +## 4.0.0 2018-2-22 + +- Introduce new MetaMask user interface. + +## 3.14.2 2018-2-15 + +- Fix bug where log subscriptions would break when switching network. +- Fix bug where storage values were cached across blocks. +- Add MetaMask light client [testing container](https://github.com/MetaMask/mesh-testing) + +## 3.14.1 2018-2-1 + +- Further fix scrolling for Firefox. + +## 3.14.0 2018-2-1 + +- Removed unneeded data from storage +- Add a "reset account" feature to Settings +- Add warning for importing some kinds of files. +- Scrollable Setting view for Firefox. + +## 3.13.8 2018-1-29 + +- Fix provider for Kovan network. +- Bump limit for EventEmitter listeners before warning. +- Display Error when empty string is entered as a token address. + +## 3.13.7 2018-1-22 + +- Add ability to bypass gas estimation loading indicator. +- Forward failed transactions to Sentry error reporting service +- Re-add changes from 3.13.5 + +## 3.13.6 2017-1-18 + +- Roll back changes to 3.13.4 to fix some issues with the new Infura REST provider. + +## 3.13.5 2018-1-16 + +- Estimating gas limit for simple ether sends now faster & cheaper, by avoiding VM usage on recipients with no code. +- Add an extra px to address for Firefox clipping. +- Fix Firefox scrollbar. +- Open metamask popup for transaction confirmation before gas estimation finishes and add a loading screen over transaction confirmation. +- Fix bug that prevented eth_signTypedData from signing bytes. +- Further improve gas price estimation. + +## 3.13.4 2018-1-9 + +- Remove recipient field if application initializes a tx with an empty string, or 0x, and tx data. Throw an error with the same condition, but without tx data. +- Improve gas price suggestion to be closer to the lowest that will be accepted. +- Throw an error if a application tries to submit a tx whose value is a decimal, and inform that it should be in wei. +- Fix bug that prevented updating custom token details. +- No longer mark long-pending transactions as failed, since we now have button to retry with higher gas. +- Fix rounding error when specifying an ether amount that has too much precision. +- Fix bug where incorrectly inputting seed phrase would prevent any future attempts from succeeding. + +## 3.13.3 2017-12-14 + +- Show tokens that are held that have no balance. +- Reduce load on Infura by using a new block polling endpoint. + +## 3.13.2 2017-12-9 + +- Reduce new block polling interval to 8000 ms, to ease server load. + +## 3.13.1 2017-12-7 + +- Allow Dapps to specify a transaction nonce, allowing dapps to propose resubmit and force-cancel transactions. + +## 3.13.0 2017-12-7 + +- Allow resubmitting transactions that are taking long to complete. + +## 3.12.1 2017-11-29 + +- Fix bug where a user could be shown two different seed phrases. +- Detect when multiple web3 extensions are active, and provide useful error. +- Adds notice about seed phrase backup. + +## 3.12.0 2017-10-25 + +- Add support for alternative ENS TLDs (Ethereum Name Service Top-Level Domains). +- Lower minimum gas price to 0.1 GWEI. +- Remove web3 injection message from production (thanks to @ChainsawBaby) +- Add additional debugging info to our state logs, specifically OS version and browser version. + +## 3.11.2 2017-10-21 + +- Fix bug where reject button would sometimes not work. +- Fixed bug where sometimes MetaMask's connection to a page would be unreliable. + +## 3.11.1 2017-10-20 + +- Fix bug where log filters were not populated correctly +- Fix bug where web3 API was sometimes injected after the page loaded. +- Fix bug where first account was sometimes not selected correctly after creating or restoring a vault. +- Fix bug where imported accounts could not use new eth_signTypedData method. + +## 3.11.0 2017-10-11 + +- Add support for new eth_signTypedData method per EIP 712. +- Fix bug where some transactions would be shown as pending forever, even after successfully mined. +- Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined. +- Fix link to support article on token addresses. + +## 3.10.9 2017-10-5 + +- Only rebrodcast transactions for a day not a days worth of blocks +- Remove Slack link from info page, since it is a big phishing target. +- Stop computing balance based on pending transactions, to avoid edge case where users are unable to send transactions. + +## 3.10.8 2017-9-28 + +- Fixed usage of new currency fetching API. + +## 3.10.7 2017-9-28 + +- Fixed bug where sometimes the current account was not correctly set and exposed to web apps. +- Added AUD, HKD, SGD, IDR, PHP to currency conversion list + +## 3.10.6 2017-9-27 + +- Fix bug where newly created accounts were not selected. +- Fix bug where selected account was not persisted between lockings. + +## 3.10.5 2017-9-27 + +- Fix block gas limit estimation. + +## 3.10.4 2017-9-27 + +- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) +- Fix memory leak warning. +- Fix bug where new event filters would not include historical events. + +## 3.10.3 2017-9-21 + +- Fix bug where metamask-dapp connections are lost on rpc error +- Fix bug that would sometimes display transactions as failed that could be successfully mined. + +## 3.10.2 2017-9-18 + +rollback to 3.10.0 due to bug + +## 3.10.1 2017-9-18 + +- Add ability to export private keys as a file. +- Add ability to export seed words as a file. +- Changed state logs to a file download than a clipboard copy. +- Add specific error for failed recipient address checksum. +- Fixed a long standing memory leak associated with filters installed by dapps +- Fix link to support center. +- Fixed tooltip icon locations to avoid overflow. +- Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher +- Sort currencies by currency name (thanks to strelok1: https://github.com/strelok1). + +## 3.10.0 2017-9-11 + +- Readded loose keyring label back into the account list. +- Remove cryptonator from chrome permissions. +- Add info on token contract addresses. +- Add validation preventing users from inputting their own addresses as token tracking addresses. +- Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94) + + +## 3.9.13 2017-9-8 + +- Changed the way we initialize the inpage provider to fix a bug affecting some developers. + +## 3.9.12 2017-9-6 + +- Fix bug that prevented Web3 1.0 compatibility +- Make eth_sign deprecation warning less noisy +- Add useful link to eth_sign deprecation warning. +- Fix bug with network version serialization over synchronous RPC +- Add MetaMask version to state logs. +- Add the total amount of tokens when multiple tokens are added under the token list +- Use HTTPS links for Etherscan. +- Update Support center link to new one with HTTPS. +- Make web3 deprecation notice more useful by linking to a descriptive article. + +## 3.9.11 2017-8-24 + +- Fix nonce calculation bug that would sometimes generate very wrong nonces. +- Give up resubmitting a transaction after 3500 blocks. + +## 3.9.10 2017-8-23 + +- Improve nonce calculation, to prevent bug where people are unable to send transactions reliably. +- Remove link to eth-tx-viz from identicons in tx history. + +## 3.9.9 2017-8-18 + +- Fix bug where some transaction submission errors would show an empty screen. +- Fix bug that could mis-render token balances when very small. +- Fix formatting of eth_sign "Sign Message" view. +- Add deprecation warning to eth_sign "Sign Message" view. + +## 3.9.8 2017-8-16 + +- Reenable token list. +- Remove default tokens. + +## 3.9.7 2017-8-15 + +- hotfix - disable token list +- Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0 + +## 3.9.6 2017-8-09 + +- Replace account screen with an account drop-down menu. +- Replace account buttons with a new account-specific drop-down menu. + +## 3.9.5 2017-8-04 + +- Improved phishing detection configuration update rate + +## 3.9.4 2017-8-03 + +- Fixed bug that prevented transactions from being rejected. + +## 3.9.3 2017-8-03 + +- Add support for EGO ujo token +- Continuously update blacklist for known phishing sites in background. +- Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. + +## 3.9.2 2017-7-26 + +- Fix bugs that could sometimes result in failed transactions after switching networks. +- Include stack traces in txMeta's to better understand the life cycle of transactions +- Enhance blacklister functionality to include levenshtein logic. (credit to @sogoiii and @409H for their help!) + +## 3.9.1 2017-7-19 + +- No longer automatically request 1 ropsten ether for the first account in a new vault. +- Now redirects from known malicious sites faster. +- Added a link to our new support page to the help screen. +- Fixed bug where a new transaction would be shown over the current transaction, creating a possible timing attack against user confirmation. +- Fixed bug in nonce tracker where an incorrect nonce would be calculated. +- Lowered minimum gas price to 1 Gwei. + +## 3.9.0 2017-7-12 + +- Now detects and blocks known phishing sites. + +## 3.8.6 2017-7-11 + +- Make transaction resubmission more resilient. +- No longer validate nonce client-side in retry loop. +- Fix bug where insufficient balance error was sometimes shown on successful transactions. + +## 3.8.5 2017-7-7 + +- Fix transaction resubmit logic to fail slightly less eagerly. + +## 3.8.4 2017-7-7 + +- Improve transaction resubmit logic to fail more eagerly when a user would expect it to. + +## 3.8.3 2017-7-6 + +- Re-enable default token list. +- Add origin header to dapp-bound requests to allow providers to throttle sites. +- Fix bug that could sometimes resubmit a transaction that had been stalled due to low balance after balance was restored. + +## 3.8.2 2017-7-3 + +- No longer show network loading indication on config screen, to allow selecting custom RPCs. +- Visually indicate that network spinner is a menu. +- Indicate what network is being searched for when disconnected. + +## 3.8.1 2017-6-30 + +- Temporarily disabled loading popular tokens by default to improve performance. +- Remove SEND token button until a better token sending form can be built, due to some precision issues. +- Fix precision bug in token balances. +- Cache token symbol and precisions to reduce network load. +- Transpile some newer JavaScript, restores compatibility with some older browsers. + +## 3.8.0 2017-6-28 + +- No longer stop rebroadcasting transactions +- Add list of popular tokens held to the account detail view. +- Add ability to add Tokens to token list. +- Add a warning to JSON file import. +- Add "send" link to token list, which goes to TokenFactory. +- Fix bug where slowly mined txs would sometimes be incorrectly marked as failed. +- Fix bug where badge count did not reflect personal_sign pending messages. +- Seed word confirmation wording is now scarier. +- Fix error for invalid seed words. +- Prevent users from submitting two duplicate transactions by disabling submit. +- Allow Dapps to specify gas price as hex string. +- Add button for copying state logs to clipboard. + +## 3.7.8 2017-6-12 + +- Add an `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. +- Fix ENS resolver symbol UI. + +## 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 @@ -64,7 +461,7 @@ - Add ability to import accounts in JSON file format (used by Mist, Geth, MyEtherWallet, and more!) - Fix unapproved messages not being included in extension badge. -- Fix rendering bug where the Confirm transaction view would lets you approve transactions when the account has insufficient balance. +- Fix rendering bug where the Confirm transaction view would let you approve transactions when the account has insufficient balance. ## 3.1.2 2017-1-24 @@ -87,8 +484,8 @@ ## 3.0.0 2017-1-16 - Fix seed word account generation (https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.t4i1qmmsz). -- Fix Bug where you see a empty transaction flash by on the confirm transaction view. -- Create visible difference in transaction history between a approved but not yet included in a block transaction and a transaction who has been confirmed. +- Fix Bug where you see an empty transaction flash by on the confirm transaction view. +- Create visible difference in transaction history between an approved but not yet included in a block transaction and a transaction who has been confirmed. - Fix memory leak in RPC Cache - Override RPC commands eth_syncing and web3_clientVersion - Remove certain non-essential permissions from certain builds. @@ -143,7 +540,7 @@ - Fix bug where gas estimate would sometimes be very high. - Increased our gas estimate from 100k gas to 20% of estimate. -- Fix github link on info page to point at current repository. +- Fix GitHub link on info page to point at current repository. ## 2.13.6 2016-10-26 @@ -219,7 +616,7 @@ popup notification opens up. - Block negative values from transactions. - Fixed a memory leak. - MetaMask logo now renders as super lightweight SVG, improving compatibility and performance. -- Now showing loading indication during vault unlocking, to clarify behavior for users who are experience slow unlocks. +- Now showing loading indication during vault unlocking, to clarify behavior for users who are experiencing slow unlocks. - Now only initially creates one wallet when restoring a vault, to reduce some users' confusion. ## 2.10.2 2016-09-02 @@ -251,7 +648,7 @@ popup notification opens up. - Added info link on account screen that visits Etherscan. - Fixed bug where a message signing request would be lost if the vault was locked. - Added shortcut to open MetaMask (Ctrl+Alt+M or Cmd+Opt/Alt+M) -- Prevent API calls in tests. +- Prevent API calls in tests. - Fixed bug where sign message confirmation would sometimes render blank. ## 2.9.0 2016-08-22 diff --git a/Dockerfile b/Dockerfile index d79584c15..f9ec62935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6 +FROM node:7 MAINTAINER kumavis # setup app dir diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE new file mode 100644 index 000000000..b56d08d95 --- /dev/null +++ b/ISSUE_TEMPLATE @@ -0,0 +1,15 @@ + diff --git a/LICENSE b/LICENSE index 429f4eaee..ddfbecf90 100644 --- a/LICENSE +++ b/LICENSE @@ -1,34 +1,20 @@ -Copyright (c) 2016 MetaMask +MIT License -The Ethereum Project Contributor Asset Distribution Terms ( MIT + Share-alike ) - - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and - -associated documentation files (the "Software"), to deal in the Software without restriction, - -including without limitation the rights to use, copy, modify, merge, publish, distribute, - -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +Copyright (c) 2018 MetaMask +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial - -portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT - -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR - -THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -These licence terms have been adapted from the MIT licence. \ No newline at end of file +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/README.md b/README.md index aa79f4564..d45b73778 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-plugin.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-plugin) +# MetaMask Browser Extension +[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](http://waffle.io/MetaMask/metamask-extension) + + +## Support + +If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/). ## Developing Compatible Dapps If you're a web dapp developer, we've got two types of guides for you: -- If you've never built a Dapp before, we've got a gentle introduction on [Developing Dapps with Truffle and MetaMask](https://blog.metamask.io/developing-for-metamask-with-truffle/). +### New Dapp Developers + +- We recommend this [Learning Solidity](https://karl.tech/learning-solidity-part-1-deploy-a-contract/) tutorial series by Karl Floersch. +- We wrote a (slightly outdated now) gentle introduction on [Developing Dapps with Truffle and MetaMask](https://medium.com/metamask/developing-ethereum-dapps-with-truffle-and-metamask-aa8ad7e363ba). + +### Current Dapp Developers + - If you have a Dapp, and you want to ensure compatibility, [here is our guide on building MetaMask-compatible Dapps](https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md) ## Building locally @@ -18,11 +30,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 +57,23 @@ 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 port MetaMask to a new platform](./docs/porting_to_new_environment.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 -``` diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json new file mode 100644 index 000000000..c58af4b80 --- /dev/null +++ b/app/_locales/ko/messages.json @@ -0,0 +1,10 @@ +{ + "appName": { + "message": "MetaMask", + "description": "The name of the application" + }, + "appDescription": { + "message": "이더리움 계좌 관리", + "description": "The description of the application" + } +} diff --git a/app/currencies.json b/app/currencies.json deleted file mode 100644 index 07889798b..000000000 --- a/app/currencies.json +++ /dev/null @@ -1 +0,0 @@ -{"rows":[{"code":"007","name":"007","statuses":["primary"]},{"code":"1337","name":"1337","statuses":["primary"]},{"code":"1CR","name":"1CR","statuses":["primary"]},{"code":"256","name":"256","statuses":["primary"]},{"code":"2FLAV","name":"2FLAV","statuses":["primary"]},{"code":"2GIVE","name":"2GIVE","statuses":["primary"]},{"code":"404","name":"404","statuses":["primary"]},{"code":"611","name":"611","statuses":["primary"]},{"code":"888","name":"888","statuses":["primary"]},{"code":"8BIT","name":"8Bit","statuses":["primary"]},{"code":"ACLR","name":"ACLR","statuses":["primary"]},{"code":"ACOIN","name":"ACOIN","statuses":["primary"]},{"code":"ACP","name":"ACP","statuses":["primary"]},{"code":"ADC","name":"ADC","statuses":["primary"]},{"code":"ADZ","name":"Adzcoin","statuses":["primary"]},{"code":"AEC","name":"AEC","statuses":["primary"]},{"code":"AEON","name":"Aeon","statuses":["primary"]},{"code":"AGRS","name":"Agoras Tokens","statuses":["primary"]},{"code":"AIB","name":"AIB","statuses":["primary"]},{"code":"ADN","name":"Aiden","statuses":["primary"]},{"code":"AIR","name":"AIR","statuses":["primary"]},{"code":"ALC","name":"ALC","statuses":["primary"]},{"code":"ALTC","name":"ALTC","statuses":["primary"]},{"code":"AM","name":"AM","statuses":["primary"]},{"code":"AMBER","name":"AMBER","statuses":["primary"]},{"code":"AMS","name":"AMS","statuses":["primary"]},{"code":"ANAL","name":"ANAL","statuses":["primary"]},{"code":"AND","name":"AND","statuses":["primary"]},{"code":"ANI","name":"ANI","statuses":["primary"]},{"code":"ANC","name":"Anoncoin","statuses":["primary"]},{"code":"ANTI","name":"AntiBitcoin","statuses":["primary"]},{"code":"APEX","name":"APEX","statuses":["primary"]},{"code":"APC","name":"Applecoin","statuses":["primary"]},{"code":"APT","name":"APT","statuses":["primary"]},{"code":"AR2","name":"AR2","statuses":["primary"]},{"code":"ARB","name":"ARB","statuses":["primary"]},{"code":"ARC","name":"ARC","statuses":["primary"]},{"code":"ARCH","name":"ARCH","statuses":["primary"]},{"code":"ABY","name":"ArtByte","statuses":["primary"]},{"code":"ARTC","name":"ARTC","statuses":["primary"]},{"code":"ADCN","name":"Asiadigicoin","statuses":["primary"]},{"code":"ATEN","name":"ATEN","statuses":["primary"]},{"code":"REP","name":"Augur","statuses":["primary"]},{"code":"AUR","name":"Auroracoin","statuses":["primary"]},{"code":"AUD","name":"Australian Dollar","statuses":["secondary"]},{"code":"AV","name":"AV","statuses":["primary"]},{"code":"BA","name":"BA","statuses":["primary"]},{"code":"BAC","name":"BAC","statuses":["primary"]},{"code":"BTA","name":"Bata","statuses":["primary"]},{"code":"BAY","name":"BAY","statuses":["primary"]},{"code":"BBCC","name":"BBCC","statuses":["primary"]},{"code":"BQC","name":"BBQCoin","statuses":["primary"]},{"code":"BDC","name":"BDC","statuses":["primary"]},{"code":"BEC","name":"BEC","statuses":["primary"]},{"code":"BEEZ","name":"BEEZ","statuses":["primary"]},{"code":"BELA","name":"BellaCoin","statuses":["primary"]},{"code":"BERN","name":"BERNcash","statuses":["primary"]},{"code":"BILL","name":"BILL","statuses":["primary"]},{"code":"BILS","name":"BILS","statuses":["primary"]},{"code":"BIOS","name":"BiosCrypto","statuses":["primary"]},{"code":"BIT","name":"BIT","statuses":["primary"]},{"code":"BIT16","name":"BIT16","statuses":["primary"]},{"code":"BITB","name":"BitBean","statuses":["primary"]},{"code":"BTC","name":"Bitcoin","statuses":["primary","secondary"]},{"code":"XBC","name":"Bitcoin Plus","statuses":["primary"]},{"code":"BTCD","name":"BitcoinDark","statuses":["primary"]},{"code":"BCY","name":"Bitcrystals","statuses":["primary"]},{"code":"BTM","name":"Bitmark","statuses":["primary"]},{"code":"BTQ","name":"BitQuark","statuses":["primary"]},{"code":"BITS","name":"BITS","statuses":["primary"]},{"code":"BSD","name":"BitSend","statuses":["primary"]},{"code":"BTS","name":"BitShares","statuses":["primary"]},{"code":"PTS","name":"BitShares PTS","statuses":["primary"]},{"code":"SWIFT","name":"BitSwift","statuses":["primary"]},{"code":"BITZ","name":"Bitz","statuses":["primary"]},{"code":"BLK","name":"Blackcoin","statuses":["primary"]},{"code":"JACK","name":"BlackJack","statuses":["primary"]},{"code":"BLC","name":"Blakecoin","statuses":["primary"]},{"code":"BLEU","name":"BLEU","statuses":["primary"]},{"code":"BLITZ","name":"Blitzcoin","statuses":["primary"]},{"code":"BLOCK","name":"Blocknet","statuses":["primary"]},{"code":"BLRY","name":"BLRY","statuses":["primary"]},{"code":"BLU","name":"BLU","statuses":["primary"]},{"code":"BM","name":"BM","statuses":["primary"]},{"code":"BNT","name":"BNT","statuses":["primary"]},{"code":"BOB","name":"BOB","statuses":["primary"]},{"code":"BON","name":"BON","statuses":["primary"]},{"code":"BBR","name":"Boolberry","statuses":["primary"]},{"code":"BOST","name":"BoostCoin","statuses":["primary"]},{"code":"BOSS","name":"BOSS","statuses":["primary"]},{"code":"BPOK","name":"BPOK","statuses":["primary"]},{"code":"BRAIN","name":"BRAIN","statuses":["primary"]},{"code":"BRC","name":"BRC","statuses":["primary"]},{"code":"BRDD","name":"BRDD","statuses":["primary"]},{"code":"BRIT","name":"BRIT","statuses":["primary"]},{"code":"GBP","name":"British Pound Sterling","statuses":["secondary"]},{"code":"BRK","name":"BRK","statuses":["primary"]},{"code":"BRX","name":"BRX","statuses":["primary"]},{"code":"BSC","name":"BSC","statuses":["primary"]},{"code":"BST","name":"BST","statuses":["primary"]},{"code":"BTCHC","name":"BTCHC","statuses":["primary"]},{"code":"BTCR","name":"BTCR","statuses":["primary"]},{"code":"BTCS","name":"BTCS","statuses":["primary"]},{"code":"BTCU","name":"BTCU","statuses":["primary"]},{"code":"BTTF","name":"BTTF","statuses":["primary"]},{"code":"BTX","name":"BTX","statuses":["primary"]},{"code":"BUCKS","name":"BUCKS","statuses":["primary"]},{"code":"BUN","name":"BUN","statuses":["primary"]},{"code":"BURST","name":"Burst","statuses":["primary"]},{"code":"BUZZ","name":"BUZZ","statuses":["primary"]},{"code":"BVC","name":"BVC","statuses":["primary"]},{"code":"BYC","name":"Bytecent","statuses":["primary"]},{"code":"BCN","name":"Bytecoin","statuses":["primary"]},{"code":"XCT","name":"C-Bit","statuses":["primary"]},{"code":"C0C0","name":"C0C0","statuses":["primary"]},{"code":"CAB","name":"Cabbage Unit","statuses":["primary"]},{"code":"CAD","name":"CAD","statuses":["primary","secondary"]},{"code":"CAGE","name":"CAGE","statuses":["primary"]},{"code":"CANN","name":"CannabisCoin","statuses":["primary"]},{"code":"CCN","name":"Cannacoin","statuses":["primary"]},{"code":"CPC","name":"Capricoin","statuses":["primary"]},{"code":"DIEM","name":"CarpeDiemCoin","statuses":["primary"]},{"code":"CASH","name":"CASH","statuses":["primary"]},{"code":"CBIT","name":"CBIT","statuses":["primary"]},{"code":"CC","name":"CC","statuses":["primary"]},{"code":"CCB","name":"CCB","statuses":["primary"]},{"code":"CD","name":"CD","statuses":["primary"]},{"code":"CDN","name":"CDN","statuses":["primary"]},{"code":"CF","name":"CF","statuses":["primary"]},{"code":"CFC","name":"CFC","statuses":["primary"]},{"code":"CGA","name":"CGA","statuses":["primary"]},{"code":"CHC","name":"CHC","statuses":["primary"]},{"code":"CKC","name":"Checkcoin","statuses":["primary"]},{"code":"CHEMX","name":"CHEMX","statuses":["primary"]},{"code":"CHESS","name":"CHESS","statuses":["primary"]},{"code":"CHF","name":"CHF","statuses":["primary","secondary"]},{"code":"CNY","name":"Chinese Yuan","statuses":["secondary"]},{"code":"CHRG","name":"CHRG","statuses":["primary"]},{"code":"CJ","name":"CJ","statuses":["primary"]},{"code":"CLAM","name":"Clams","statuses":["primary"]},{"code":"CLICK","name":"CLICK","statuses":["primary"]},{"code":"CLINT","name":"CLINT","statuses":["primary"]},{"code":"CLOAK","name":"Cloakcoin","statuses":["primary"]},{"code":"CLR","name":"CLR","statuses":["primary"]},{"code":"CLUB","name":"CLUB","statuses":["primary"]},{"code":"CLUD","name":"CLUD","statuses":["primary"]},{"code":"CMT","name":"CMT","statuses":["primary"]},{"code":"CNC","name":"CNC","statuses":["primary"]},{"code":"COXST","name":"CoExistCoin","statuses":["primary"]},{"code":"COIN","name":"COIN","statuses":["primary"]},{"code":"C2","name":"Coin2.1","statuses":["primary"]},{"code":"CNMT","name":"Coinomat","statuses":["primary"]},{"code":"CV2","name":"Colossuscoin2.0","statuses":["primary"]},{"code":"CON","name":"CON","statuses":["primary"]},{"code":"XCP","name":"Counterparty","statuses":["primary"]},{"code":"COV","name":"COV","statuses":["primary"]},{"code":"CRAFT","name":"CRAFT","statuses":["primary"]},{"code":"CRAVE","name":"CRAVE","statuses":["primary"]},{"code":"CRC","name":"CRC","statuses":["primary"]},{"code":"CRE","name":"CRE","statuses":["primary"]},{"code":"CRBIT","name":"Creditbit","statuses":["primary"]},{"code":"CREVA","name":"CrevaCoin","statuses":["primary"]},{"code":"CRIME","name":"CRIME","statuses":["primary"]},{"code":"CRT","name":"CRT","statuses":["primary"]},{"code":"CRW","name":"CRW","statuses":["primary"]},{"code":"CRY","name":"CRY","statuses":["primary"]},{"code":"XCR","name":"Crypti","statuses":["primary"]},{"code":"CBX","name":"Crypto Bullion","statuses":["primary"]},{"code":"CESC","name":"CryptoEscudo","statuses":["primary"]},{"code":"XCN","name":"Cryptonite","statuses":["primary"]},{"code":"CSMIC","name":"CSMIC","statuses":["primary"]},{"code":"CST","name":"CST","statuses":["primary"]},{"code":"CTC","name":"CTC","statuses":["primary"]},{"code":"CTO","name":"CTO","statuses":["primary"]},{"code":"CURE","name":"Curecoin","statuses":["primary"]},{"code":"CYP","name":"Cypher","statuses":["primary"]},{"code":"CZC","name":"CZC","statuses":["primary"]},{"code":"CZECO","name":"CZECO","statuses":["primary"]},{"code":"CZR","name":"CZR","statuses":["primary"]},{"code":"DAO","name":"DAO","statuses":["primary"]},{"code":"DGD","name":"DarkGoldCoin","statuses":["primary"]},{"code":"DNET","name":"Darknet","statuses":["primary"]},{"code":"DASH","name":"Dash","statuses":["primary"]},{"code":"DTC","name":"Datacoin","statuses":["primary"]},{"code":"DBG","name":"DBG","statuses":["primary"]},{"code":"DBLK","name":"DBLK","statuses":["primary"]},{"code":"DBTC","name":"DBTC","statuses":["primary"]},{"code":"DCK","name":"DCK","statuses":["primary"]},{"code":"DCR","name":"Decred","statuses":["primary"]},{"code":"DES","name":"Destiny","statuses":["primary"]},{"code":"DETH","name":"DETH","statuses":["primary"]},{"code":"DEUR","name":"DEUR","statuses":["primary"]},{"code":"DEM","name":"Deutsche eMark","statuses":["primary"]},{"code":"DVC","name":"Devcoin","statuses":["primary"]},{"code":"DGCS","name":"DGCS","statuses":["primary"]},{"code":"DGMS","name":"DGMS","statuses":["primary"]},{"code":"DGORE","name":"DGORE","statuses":["primary"]},{"code":"DMD","name":"Diamond","statuses":["primary"]},{"code":"DGB","name":"Digibyte","statuses":["primary"]},{"code":"CUBE","name":"DigiCube","statuses":["primary"]},{"code":"DGC","name":"Digitalcoin","statuses":["primary"]},{"code":"XDN","name":"DigitalNote","statuses":["primary"]},{"code":"DP","name":"DigitalPrice","statuses":["primary"]},{"code":"DIGS","name":"DIGS","statuses":["primary"]},{"code":"DIME","name":"Dimecoin","statuses":["primary"]},{"code":"DISK","name":"DISK","statuses":["primary"]},{"code":"DLISK","name":"DLISK","statuses":["primary"]},{"code":"NOTE","name":"DNotes","statuses":["primary"]},{"code":"DOGE","name":"DOGE","statuses":["primary","secondary"]},{"code":"DOGE","name":"Dogecoin","statuses":["primary","secondary"]},{"code":"DON","name":"DON","statuses":["primary"]},{"code":"DOPE","name":"DopeCoin","statuses":["primary"]},{"code":"DOX","name":"DOX","statuses":["primary"]},{"code":"DRACO","name":"DRACO","statuses":["primary"]},{"code":"DRM","name":"DRM","statuses":["primary"]},{"code":"DROP","name":"DROP","statuses":["primary"]},{"code":"DRZ","name":"DRZ","statuses":["primary"]},{"code":"DSH","name":"DSH","statuses":["primary"]},{"code":"DBIC","name":"DubaiCoin","statuses":["primary"]},{"code":"DUO","name":"DUO","statuses":["primary"]},{"code":"DUST","name":"DUST","statuses":["primary"]},{"code":"EAC","name":"Earthcoin","statuses":["primary"]},{"code":"ECCHI","name":"ECCHI","statuses":["primary"]},{"code":"ECC","name":"ECCoin","statuses":["primary"]},{"code":"ECOS","name":"ECOS","statuses":["primary"]},{"code":"EDC","name":"EDC","statuses":["primary"]},{"code":"EDRC","name":"EDRC","statuses":["primary"]},{"code":"EGG","name":"EGG","statuses":["primary"]},{"code":"EMC2","name":"Einsteinium","statuses":["primary"]},{"code":"EKO","name":"EKO","statuses":["primary"]},{"code":"EL","name":"EL","statuses":["primary"]},{"code":"ELCO","name":"ELcoin","statuses":["primary"]},{"code":"ELE","name":"ELE","statuses":["primary"]},{"code":"EFL","name":"Electronic Gulden","statuses":["primary"]},{"code":"EMC","name":"Emercoin","statuses":["primary"]},{"code":"EMIRG","name":"EMIRG","statuses":["primary"]},{"code":"ENE","name":"ENE","statuses":["primary"]},{"code":"ENRG","name":"Energycoin","statuses":["primary"]},{"code":"EPC","name":"EPC","statuses":["primary"]},{"code":"EPY","name":"EPY","statuses":["primary"]},{"code":"ERC","name":"ERC","statuses":["primary"]},{"code":"ERC3","name":"ERC3","statuses":["primary"]},{"code":"ESC","name":"ESC","statuses":["primary"]},{"code":"ETH","name":"Ethereum","statuses":["primary","secondary"]},{"code":"ETC","name":"Ethereum Classic","statuses":["primary"]},{"code":"ETHS","name":"ETHS","statuses":["primary"]},{"code":"EURC","name":"EURC","statuses":["primary"]},{"code":"EUR","name":"Euro","statuses":["primary","secondary"]},{"code":"EGC","name":"EvergreenCoin","statuses":["primary"]},{"code":"EVIL","name":"EVIL","statuses":["primary"]},{"code":"EVO","name":"EVO","statuses":["primary"]},{"code":"EXCL","name":"EXCL","statuses":["primary"]},{"code":"EXIT","name":"EXIT","statuses":["primary"]},{"code":"EXP","name":"Expanse","statuses":["primary"]},{"code":"FCT","name":"Factom","statuses":["primary"]},{"code":"FAIR","name":"Faircoin","statuses":["primary"]},{"code":"FC2","name":"FC2","statuses":["primary"]},{"code":"FCN","name":"FCN","statuses":["primary"]},{"code":"FTC","name":"Feathercoin","statuses":["primary"]},{"code":"TIPS","name":"Fedoracoin","statuses":["primary"]},{"code":"FFC","name":"FFC","statuses":["primary"]},{"code":"FIBRE","name":"Fibre","statuses":["primary"]},{"code":"FIT","name":"FIT","statuses":["primary"]},{"code":"FJC","name":"FJC","statuses":["primary"]},{"code":"FLO","name":"Florincoin","statuses":["primary"]},{"code":"FLOZ","name":"FLOZ","statuses":["primary"]},{"code":"FLT","name":"FlutterCoin","statuses":["primary"]},{"code":"FLX","name":"FLX","statuses":["primary"]},{"code":"FLY","name":"Flycoin","statuses":["primary"]},{"code":"FLDC","name":"FoldingCoin","statuses":["primary"]},{"code":"FONZ","name":"FONZ","statuses":["primary"]},{"code":"FRK","name":"Franko","statuses":["primary"]},{"code":"FRC","name":"Freicoin","statuses":["primary"]},{"code":"FRN","name":"FRN","statuses":["primary"]},{"code":"FRWC","name":"FRWC","statuses":["primary"]},{"code":"FSC2","name":"FSC2","statuses":["primary"]},{"code":"FST","name":"FST","statuses":["primary"]},{"code":"FTP","name":"FTP","statuses":["primary"]},{"code":"FUN","name":"FUN","statuses":["primary"]},{"code":"FUTC","name":"FUTC","statuses":["primary"]},{"code":"FUZZ","name":"FUZZ","statuses":["primary"]},{"code":"GAIA","name":"GAIA","statuses":["primary"]},{"code":"GAIN","name":"GAIN","statuses":["primary"]},{"code":"GAKH","name":"GAKH","statuses":["primary"]},{"code":"GAM","name":"GAM","statuses":["primary"]},{"code":"GBT","name":"GameBet Coin","statuses":["primary"]},{"code":"GAME","name":"GameCredits","statuses":["primary"]},{"code":"GAP","name":"Gapcoin","statuses":["primary"]},{"code":"GARY","name":"GARY","statuses":["primary"]},{"code":"GB","name":"GB","statuses":["primary"]},{"code":"GBC","name":"GBC","statuses":["primary"]},{"code":"GBIT","name":"GBIT","statuses":["primary"]},{"code":"GCC","name":"GCC","statuses":["primary"]},{"code":"GCN","name":"GCN","statuses":["primary"]},{"code":"GEO","name":"GeoCoin","statuses":["primary"]},{"code":"GEMZ","name":"GetGems","statuses":["primary"]},{"code":"GHOST","name":"GHOST","statuses":["primary"]},{"code":"GHS","name":"GHS","statuses":["primary"]},{"code":"GIFT","name":"GIFT","statuses":["primary"]},{"code":"GIG","name":"GIG","statuses":["primary"]},{"code":"GLC","name":"GLC","statuses":["primary"]},{"code":"BSTY","name":"GlobalBoost-Y","statuses":["primary"]},{"code":"GML","name":"GML","statuses":["primary"]},{"code":"GMX","name":"GMX","statuses":["primary"]},{"code":"GCR","name":"GoCoineR","statuses":["primary"]},{"code":"GLD","name":"GoldCoin","statuses":["primary"]},{"code":"GOON","name":"GOON","statuses":["primary"]},{"code":"GP","name":"GP","statuses":["primary"]},{"code":"GPU","name":"GPU","statuses":["primary"]},{"code":"GRAM","name":"GRAM","statuses":["primary"]},{"code":"GRT","name":"Grantcoin","statuses":["primary"]},{"code":"GRE","name":"GRE","statuses":["primary"]},{"code":"GRC","name":"Gridcoin","statuses":["primary"]},{"code":"GRN","name":"GRN","statuses":["primary"]},{"code":"GRS","name":"Groestlcoin","statuses":["primary"]},{"code":"GRW","name":"GRW","statuses":["primary"]},{"code":"GSM","name":"GSM","statuses":["primary"]},{"code":"GSX","name":"GSX","statuses":["primary"]},{"code":"GUA","name":"GUA","statuses":["primary"]},{"code":"NLG","name":"Gulden","statuses":["primary"]},{"code":"GUN","name":"GUN","statuses":["primary"]},{"code":"HAM","name":"HAM","statuses":["primary"]},{"code":"HAWK","name":"HAWK","statuses":["primary"]},{"code":"HCC","name":"HCC","statuses":["primary"]},{"code":"HEAT","name":"HEAT","statuses":["primary"]},{"code":"HMP","name":"HempCoin","statuses":["primary"]},{"code":"XHI","name":"HiCoin","statuses":["primary"]},{"code":"HIFUN","name":"HIFUN","statuses":["primary"]},{"code":"HILL","name":"HILL","statuses":["primary"]},{"code":"HIRE","name":"HIRE","statuses":["primary"]},{"code":"HNC","name":"HNC","statuses":["primary"]},{"code":"HODL","name":"HOdlcoin","statuses":["primary"]},{"code":"HKD","name":"Hong Kong Dollar","statuses":["secondary"]},{"code":"HZ","name":"Horizon","statuses":["primary"]},{"code":"HTC","name":"HTC","statuses":["primary"]},{"code":"HTML5","name":"HTMLCOIN","statuses":["primary"]},{"code":"HUC","name":"HUC","statuses":["primary"]},{"code":"HVCO","name":"HVCO","statuses":["primary"]},{"code":"HYPER","name":"Hyper","statuses":["primary"]},{"code":"HYP","name":"HyperStake","statuses":["primary"]},{"code":"I0C","name":"I0C","statuses":["primary"]},{"code":"IBANK","name":"IBANK","statuses":["primary"]},{"code":"ICASH","name":"iCash","statuses":["primary"]},{"code":"ICN","name":"ICN","statuses":["primary"]},{"code":"IEC","name":"IEC","statuses":["primary"]},{"code":"IFC","name":"Infinitecoin","statuses":["primary"]},{"code":"INFX","name":"Influxcoin","statuses":["primary"]},{"code":"INV","name":"INV","statuses":["primary"]},{"code":"IOC","name":"IO Coin","statuses":["primary"]},{"code":"ION","name":"ION","statuses":["primary"]},{"code":"IRL","name":"IRL","statuses":["primary"]},{"code":"ISL","name":"IslaCoin","statuses":["primary"]},{"code":"IVZ","name":"IVZ","statuses":["primary"]},{"code":"IXC","name":"IXC","statuses":["primary"]},{"code":"JIF","name":"JIF","statuses":["primary"]},{"code":"JPC","name":"JPC","statuses":["primary"]},{"code":"JPY","name":"JPY","statuses":["primary","secondary"]},{"code":"JBS","name":"Jumbucks","statuses":["primary"]},{"code":"KAT","name":"KAT","statuses":["primary"]},{"code":"KGC","name":"KGC","statuses":["primary"]},{"code":"KNC","name":"KhanCoin","statuses":["primary"]},{"code":"KLC","name":"KLC","statuses":["primary"]},{"code":"KOBO","name":"KOBO","statuses":["primary"]},{"code":"KORE","name":"KoreCoin","statuses":["primary"]},{"code":"KRAK","name":"KRAK","statuses":["primary"]},{"code":"KRYP","name":"KRYP","statuses":["primary"]},{"code":"KR","name":"Krypton","statuses":["primary"]},{"code":"KTK","name":"KTK","statuses":["primary"]},{"code":"KUBO","name":"KUBO","statuses":["primary"]},{"code":"LANA","name":"LANA","statuses":["primary"]},{"code":"LBC","name":"LBC","statuses":["primary"]},{"code":"LC","name":"LC","statuses":["primary"]},{"code":"LEA","name":"LeaCoin","statuses":["primary"]},{"code":"LEMON","name":"LEMON","statuses":["primary"]},{"code":"LEO","name":"LEO","statuses":["primary"]},{"code":"LFC","name":"LFC","statuses":["primary"]},{"code":"LFO","name":"LFO","statuses":["primary"]},{"code":"LFTC","name":"LFTC","statuses":["primary"]},{"code":"LQD","name":"LIQUID","statuses":["primary"]},{"code":"LIR","name":"LIR","statuses":["primary"]},{"code":"LSK","name":"Lisk","statuses":["primary"]},{"code":"LTC","name":"Litecoin","statuses":["primary","secondary"]},{"code":"LTCR","name":"Litecred","statuses":["primary"]},{"code":"LDOGE","name":"LiteDoge","statuses":["primary"]},{"code":"LKC","name":"LKC","statuses":["primary"]},{"code":"LOC","name":"LOC","statuses":["primary"]},{"code":"LOOT","name":"LOOT","statuses":["primary"]},{"code":"LTBC","name":"LTBcoin","statuses":["primary"]},{"code":"LTC","name":"LTC","statuses":["primary","secondary"]},{"code":"LTH","name":"LTH","statuses":["primary"]},{"code":"LTS","name":"LTS","statuses":["primary"]},{"code":"LUN","name":"LUN","statuses":["primary"]},{"code":"LXC","name":"LXC","statuses":["primary"]},{"code":"LYB","name":"LYB","statuses":["primary"]},{"code":"M1","name":"M1","statuses":["primary"]},{"code":"MAD","name":"MAD","statuses":["primary"]},{"code":"XMG","name":"Magi","statuses":["primary"]},{"code":"MAID","name":"MaidSafeCoin","statuses":["primary"]},{"code":"MXT","name":"MarteXcoin","statuses":["primary"]},{"code":"MARV","name":"MARV","statuses":["primary"]},{"code":"MARYJ","name":"MARYJ","statuses":["primary"]},{"code":"OMNI","name":"Mastercoin (Omni)","statuses":["primary"]},{"code":"MTR","name":"MasterTraderCoin","statuses":["primary"]},{"code":"MAX","name":"Maxcoin","statuses":["primary"]},{"code":"MZC","name":"Mazacoin","statuses":["primary"]},{"code":"MBL","name":"MBL","statuses":["primary"]},{"code":"MCAR","name":"MCAR","statuses":["primary"]},{"code":"MCN","name":"MCN","statuses":["primary"]},{"code":"MCZ","name":"MCZ","statuses":["primary"]},{"code":"MED","name":"MediterraneanCoin","statuses":["primary"]},{"code":"MEC","name":"Megacoin","statuses":["primary"]},{"code":"MEME","name":"Memetic","statuses":["primary"]},{"code":"METAL","name":"METAL","statuses":["primary"]},{"code":"MND","name":"MindCoin","statuses":["primary"]},{"code":"MINT","name":"Mintcoin","statuses":["primary"]},{"code":"MIS","name":"MIS","statuses":["primary"]},{"code":"MM","name":"MM","statuses":["primary"]},{"code":"MMC","name":"MMC","statuses":["primary"]},{"code":"MMNXT","name":"MMNXT","statuses":["primary"]},{"code":"MMXVI","name":"MMXVI","statuses":["primary"]},{"code":"MNM","name":"MNM","statuses":["primary"]},{"code":"MOIN","name":"MOIN","statuses":["primary"]},{"code":"MOJO","name":"MojoCoin","statuses":["primary"]},{"code":"MONA","name":"MonaCoin","statuses":["primary"]},{"code":"XMR","name":"Monero","statuses":["primary","secondary"]},{"code":"MNTA","name":"Moneta","statuses":["primary"]},{"code":"MUE","name":"MonetaryUnit","statuses":["primary"]},{"code":"MOON","name":"Mooncoin","statuses":["primary"]},{"code":"MOOND","name":"MOOND","statuses":["primary"]},{"code":"MOTO","name":"MOTO","statuses":["primary"]},{"code":"MPRO","name":"MPRO","statuses":["primary"]},{"code":"MRB","name":"MRB","statuses":["primary"]},{"code":"MRP","name":"MRP","statuses":["primary"]},{"code":"MSC","name":"MSC","statuses":["primary"]},{"code":"MYR","name":"Myriadcoin","statuses":["primary"]},{"code":"NMC","name":"Namecoin","statuses":["primary"]},{"code":"NAUT","name":"Nautiluscoin","statuses":["primary"]},{"code":"NAV","name":"NAV Coin","statuses":["primary"]},{"code":"NCS","name":"NCS","statuses":["primary"]},{"code":"XEM","name":"NEM","statuses":["primary"]},{"code":"NEOS","name":"NeosCoin","statuses":["primary"]},{"code":"NETC","name":"NETC","statuses":["primary"]},{"code":"NET","name":"NetCoin","statuses":["primary"]},{"code":"NEU","name":"NeuCoin","statuses":["primary"]},{"code":"NTRN","name":"Neutron","statuses":["primary"]},{"code":"NEVA","name":"NevaCoin","statuses":["primary"]},{"code":"NEWB","name":"NEWB","statuses":["primary"]},{"code":"NIRO","name":"Nexus","statuses":["primary"]},{"code":"NIC","name":"NIC","statuses":["primary"]},{"code":"NKA","name":"NKA","statuses":["primary"]},{"code":"NKC","name":"NKC","statuses":["primary"]},{"code":"NOBL","name":"NobleCoin","statuses":["primary"]},{"code":"NODE","name":"NODE","statuses":["primary"]},{"code":"NODES","name":"NODES","statuses":["primary"]},{"code":"NOO","name":"NOO","statuses":["primary"]},{"code":"NVC","name":"Novacoin","statuses":["primary"]},{"code":"NRC","name":"NRC","statuses":["primary"]},{"code":"NRS","name":"NRS","statuses":["primary"]},{"code":"NUBIS","name":"NUBIS","statuses":["primary"]},{"code":"NBT","name":"NuBits","statuses":["primary"]},{"code":"NUM","name":"NUM","statuses":["primary"]},{"code":"NSR","name":"NuShares","statuses":["primary"]},{"code":"NXE","name":"NXE","statuses":["primary"]},{"code":"NXT","name":"NXT","statuses":["primary"]},{"code":"NXTTY","name":"Nxttycoin","statuses":["primary"]},{"code":"NYC","name":"NYC","statuses":["primary"]},{"code":"NZC","name":"NZC","statuses":["primary"]},{"code":"NZD","name":"NZD","statuses":["primary","secondary"]},{"code":"OC","name":"OC","statuses":["primary"]},{"code":"OCOW","name":"OCOW","statuses":["primary"]},{"code":"OK","name":"OKCash","statuses":["primary"]},{"code":"OMA","name":"OMA","statuses":["primary"]},{"code":"ONE","name":"ONE","statuses":["primary"]},{"code":"ONEC","name":"ONEC","statuses":["primary"]},{"code":"OP","name":"OP","statuses":["primary"]},{"code":"OPAL","name":"OPAL","statuses":["primary"]},{"code":"OPES","name":"OPES","statuses":["primary"]},{"code":"ORB","name":"Orbitcoin","statuses":["primary"]},{"code":"ORLY","name":"Orlycoin","statuses":["primary"]},{"code":"OS76","name":"OS76","statuses":["primary"]},{"code":"OZC","name":"OZC","statuses":["primary"]},{"code":"PAC","name":"PAC","statuses":["primary"]},{"code":"PAK","name":"PAK","statuses":["primary"]},{"code":"PND","name":"Pandacoin","statuses":["primary"]},{"code":"PAPAF","name":"PAPAF","statuses":["primary"]},{"code":"XPY","name":"Paycoin","statuses":["primary"]},{"code":"PBC","name":"PBC","statuses":["primary"]},{"code":"PDC","name":"PDC","statuses":["primary"]},{"code":"XPB","name":"Pebblecoin","statuses":["primary"]},{"code":"PPC","name":"Peercoin","statuses":["primary"]},{"code":"PEN","name":"PEN","statuses":["primary"]},{"code":"PHR","name":"PHR","statuses":["primary"]},{"code":"PIGGY","name":"Piggycoin","statuses":["primary"]},{"code":"PC","name":"Pinkcoin","statuses":["primary"]},{"code":"PKB","name":"PKB","statuses":["primary"]},{"code":"PLN","name":"PLN","statuses":["primary","secondary"]},{"code":"PLNC","name":"PLNC","statuses":["primary"]},{"code":"PNC","name":"PNC","statuses":["primary"]},{"code":"PNK","name":"PNK","statuses":["primary"]},{"code":"POKE","name":"POKE","statuses":["primary"]},{"code":"PONZ2","name":"PONZ2","statuses":["primary"]},{"code":"PONZI","name":"PONZI","statuses":["primary"]},{"code":"PEX","name":"PosEx","statuses":["primary"]},{"code":"POST","name":"POST","statuses":["primary"]},{"code":"POT","name":"Potcoin","statuses":["primary"]},{"code":"PRES","name":"PRES","statuses":["primary"]},{"code":"PXI","name":"Prime-XI","statuses":["primary"]},{"code":"PRIME","name":"PrimeChain","statuses":["primary"]},{"code":"XPM","name":"Primecoin","statuses":["primary"]},{"code":"PRM","name":"PRM","statuses":["primary"]},{"code":"PRT","name":"PRT","statuses":["primary"]},{"code":"PSP","name":"PSP","statuses":["primary"]},{"code":"PTC","name":"PTC","statuses":["primary"]},{"code":"PULSE","name":"PULSE","statuses":["primary"]},{"code":"PURE","name":"PURE","statuses":["primary"]},{"code":"PUTIN","name":"PUTIN","statuses":["primary"]},{"code":"PWR","name":"PWR","statuses":["primary"]},{"code":"PXL","name":"PXL","statuses":["primary"]},{"code":"QBC","name":"QBC","statuses":["primary"]},{"code":"QBK","name":"QBK","statuses":["primary"]},{"code":"QCN","name":"QCN","statuses":["primary"]},{"code":"QORA","name":"Qora","statuses":["primary"]},{"code":"QTZ","name":"QTZ","statuses":["primary"]},{"code":"QRK","name":"Quark","statuses":["primary"]},{"code":"QTL","name":"Quatloo","statuses":["primary"]},{"code":"RADI","name":"RADI","statuses":["primary"]},{"code":"RADS","name":"Radium","statuses":["primary"]},{"code":"RED","name":"RED","statuses":["primary"]},{"code":"RDD","name":"Reddcoin","statuses":["primary"]},{"code":"REE","name":"REE","statuses":["primary"]},{"code":"REV","name":"Revenu","statuses":["primary"]},{"code":"RBR","name":"RibbitRewards","statuses":["primary"]},{"code":"RICHX","name":"RICHX","statuses":["primary"]},{"code":"RIC","name":"Riecoin","statuses":["primary"]},{"code":"RBT","name":"Rimbit","statuses":["primary"]},{"code":"RIO","name":"RIO","statuses":["primary"]},{"code":"XRP","name":"Ripple","statuses":["primary"]},{"code":"RISE","name":"RISE","statuses":["primary"]},{"code":"RMS","name":"RMS","statuses":["primary"]},{"code":"RONIN","name":"RONIN","statuses":["primary"]},{"code":"ROOT","name":"ROOT","statuses":["primary"]},{"code":"ROS","name":"RosCoin","statuses":["primary"]},{"code":"RPC","name":"RPC","statuses":["primary"]},{"code":"RBIES","name":"Rubies","statuses":["primary"]},{"code":"RUBIT","name":"RUBIT","statuses":["primary"]},{"code":"RUR","name":"Ruble","statuses":["secondary"]},{"code":"RBY","name":"Rubycoin","statuses":["primary"]},{"code":"RUST","name":"RUST","statuses":["primary"]},{"code":"SEC","name":"Safe Exchange Coin","statuses":["primary"]},{"code":"SAK","name":"SAK","statuses":["primary"]},{"code":"SAR","name":"SAR","statuses":["primary"]},{"code":"SBD","name":"SBD","statuses":["primary"]},{"code":"SBIT","name":"SBIT","statuses":["primary"]},{"code":"SCAN","name":"SCAN","statuses":["primary"]},{"code":"SCOT","name":"Scotcoin","statuses":["primary"]},{"code":"SCRPT","name":"SCRPT","statuses":["primary"]},{"code":"SCRT","name":"SCRT","statuses":["primary"]},{"code":"SRC","name":"SecureCoin","statuses":["primary"]},{"code":"SXC","name":"Sexcoin","statuses":["primary"]},{"code":"SFE","name":"SFE","statuses":["primary"]},{"code":"SFR","name":"SFR","statuses":["primary"]},{"code":"SGD","name":"SGD","statuses":["primary","secondary"]},{"code":"SDC","name":"ShadowCash","statuses":["primary"]},{"code":"SHELL","name":"SHELL","statuses":["primary"]},{"code":"SHF","name":"SHF","statuses":["primary"]},{"code":"SHI","name":"SHI","statuses":["primary"]},{"code":"SHIFT","name":"Shift","statuses":["primary"]},{"code":"SHREK","name":"SHREK","statuses":["primary"]},{"code":"SC","name":"Siacoin","statuses":["primary"]},{"code":"SIB","name":"Siberian chervonets","statuses":["primary"]},{"code":"SIC","name":"SIC","statuses":["primary"]},{"code":"SIGU","name":"SIGU","statuses":["primary"]},{"code":"SILK","name":"Silkcoin","statuses":["primary"]},{"code":"SIX","name":"SIX","statuses":["primary"]},{"code":"SLING","name":"Sling","statuses":["primary"]},{"code":"SLS","name":"SLS","statuses":["primary"]},{"code":"SMBR","name":"SMBR","statuses":["primary"]},{"code":"SMC","name":"SMC","statuses":["primary"]},{"code":"SMLY","name":"SmileyCoin","statuses":["primary"]},{"code":"SNRG","name":"SNRG","statuses":["primary"]},{"code":"SOIL","name":"SOILcoin","statuses":["primary"]},{"code":"SLR","name":"Solarcoin","statuses":["primary"]},{"code":"SOLO","name":"SOLO","statuses":["primary"]},{"code":"SONG","name":"SongCoin","statuses":["primary"]},{"code":"SOON","name":"SOON","statuses":["primary"]},{"code":"SPC","name":"SPC","statuses":["primary"]},{"code":"SPEX","name":"SPEX","statuses":["primary"]},{"code":"SPHR","name":"Sphere","statuses":["primary"]},{"code":"SPM","name":"SPM","statuses":["primary"]},{"code":"SPN","name":"SPN","statuses":["primary"]},{"code":"SPOTS","name":"SPOTS","statuses":["primary"]},{"code":"SPR","name":"SpreadCoin","statuses":["primary"]},{"code":"SPRTS","name":"Sprouts","statuses":["primary"]},{"code":"SQC","name":"SQC","statuses":["primary"]},{"code":"SSC","name":"SSC","statuses":["primary"]},{"code":"SSTC","name":"SSTC","statuses":["primary"]},{"code":"STA","name":"STA","statuses":["primary"]},{"code":"START","name":"Startcoin","statuses":["primary"]},{"code":"XST","name":"Stealthcoin","statuses":["primary"]},{"code":"STEEM","name":"Steem","statuses":["primary"]},{"code":"XLM","name":"Stellar","statuses":["primary"]},{"code":"STR","name":"Stellar","statuses":["primary"]},{"code":"STEPS","name":"Steps","statuses":["primary"]},{"code":"SLG","name":"Sterlingcoin","statuses":["primary"]},{"code":"STL","name":"STL","statuses":["primary"]},{"code":"SJCX","name":"Storjcoin X","statuses":["primary"]},{"code":"STP","name":"STP","statuses":["primary"]},{"code":"STRB","name":"STRB","statuses":["primary"]},{"code":"STS","name":"Stress","statuses":["primary"]},{"code":"STRP","name":"STRP","statuses":["primary"]},{"code":"STV","name":"STV","statuses":["primary"]},{"code":"SUB","name":"Subcriptio","statuses":["primary"]},{"code":"SUPER","name":"SUPER","statuses":["primary"]},{"code":"UNITY","name":"SuperNET","statuses":["primary"]},{"code":"SWARM","name":"Swarm","statuses":["primary"]},{"code":"SWING","name":"SWING","statuses":["primary"]},{"code":"SDP","name":"SydPak Coin","statuses":["primary"]},{"code":"SYNC","name":"SYNC","statuses":["primary"]},{"code":"AMP","name":"Synereo","statuses":["primary"]},{"code":"SYS","name":"Syscoin","statuses":["primary"]},{"code":"TAG","name":"TagCoin","statuses":["primary"]},{"code":"TAJ","name":"TAJ","statuses":["primary"]},{"code":"TAK","name":"TAK","statuses":["primary"]},{"code":"TAM","name":"TAM","statuses":["primary"]},{"code":"TAO","name":"TAO","statuses":["primary"]},{"code":"TBC","name":"TBC","statuses":["primary"]},{"code":"TBCX","name":"TBCX","statuses":["primary"]},{"code":"TCR","name":"TCR","statuses":["primary"]},{"code":"TDFB","name":"TDFB","statuses":["primary"]},{"code":"TDY","name":"TDY","statuses":["primary"]},{"code":"TEK","name":"TEKcoin","statuses":["primary"]},{"code":"TRC","name":"Terracoin","statuses":["primary"]},{"code":"TESLA","name":"TESLA","statuses":["primary"]},{"code":"TES","name":"TeslaCoin","statuses":["primary"]},{"code":"TET","name":"TET","statuses":["primary"]},{"code":"USDT","name":"Tether","statuses":["primary","secondary"]},{"code":"THC","name":"THC","statuses":["primary"]},{"code":"THS","name":"THS","statuses":["primary"]},{"code":"TIX","name":"Tickets","statuses":["primary"]},{"code":"XTC","name":"TileCoin","statuses":["primary"]},{"code":"TIT","name":"Titcoin","statuses":["primary"]},{"code":"TTC","name":"TittieCoin","statuses":["primary"]},{"code":"TMC","name":"TMC","statuses":["primary"]},{"code":"TODAY","name":"TODAY","statuses":["primary"]},{"code":"TOKEN","name":"TOKEN","statuses":["primary"]},{"code":"TP1","name":"TP1","statuses":["primary"]},{"code":"TPC","name":"TPC","statuses":["primary"]},{"code":"TPG","name":"TPG","statuses":["primary"]},{"code":"TX","name":"Transfercoin","statuses":["primary"]},{"code":"TRAP","name":"TRAP","statuses":["primary"]},{"code":"TRICK","name":"TRICK","statuses":["primary"]},{"code":"TROLL","name":"TROLL","statuses":["primary"]},{"code":"TRK","name":"Truckcoin","statuses":["primary"]},{"code":"TRUMP","name":"TrumpCoin","statuses":["primary"]},{"code":"TRUST","name":"TRUST","statuses":["primary"]},{"code":"UAE","name":"UAE","statuses":["primary"]},{"code":"UFO","name":"UFO Coin","statuses":["primary"]},{"code":"UIS","name":"UIS","statuses":["primary"]},{"code":"UTC","name":"UltraCoin","statuses":["primary"]},{"code":"UNC","name":"UNC","statuses":["primary"]},{"code":"UNIQ","name":"UNIQ","statuses":["primary"]},{"code":"UNIT","name":"Universal Currency","statuses":["primary"]},{"code":"UNO","name":"Unobtanium","statuses":["primary"]},{"code":"URO","name":"Uro","statuses":["primary"]},{"code":"USD","name":"US Dollar","statuses":["primary","secondary"]},{"code":"USDE","name":"USDE","statuses":["primary"]},{"code":"UTH","name":"UTH","statuses":["primary"]},{"code":"VAL","name":"VAL","statuses":["primary"]},{"code":"XVC","name":"Vcash","statuses":["primary"]},{"code":"VCN","name":"VCN","statuses":["primary"]},{"code":"VEG","name":"VEG","statuses":["primary"]},{"code":"VENE","name":"VENE","statuses":["primary"]},{"code":"XVG","name":"Verge","statuses":["primary"]},{"code":"VRC","name":"VeriCoin","statuses":["primary"]},{"code":"VTC","name":"Vertcoin","statuses":["primary"]},{"code":"VIA","name":"Viacoin","statuses":["primary"]},{"code":"VIOR","name":"Viorcoin","statuses":["primary"]},{"code":"VIP","name":"VIP Tokens","statuses":["primary"]},{"code":"VIRAL","name":"Viral","statuses":["primary"]},{"code":"VOOT","name":"VootCoin","statuses":["primary"]},{"code":"VOX","name":"Voxels","statuses":["primary"]},{"code":"VOYA","name":"VOYA","statuses":["primary"]},{"code":"VPN","name":"VPNCoin","statuses":["primary"]},{"code":"VPRC","name":"VPRC","statuses":["primary"]},{"code":"VTA","name":"VTA","statuses":["primary"]},{"code":"VTN","name":"VTN","statuses":["primary"]},{"code":"VTR","name":"VTR","statuses":["primary"]},{"code":"WAC","name":"WAC","statuses":["primary"]},{"code":"WARP","name":"WARP","statuses":["primary"]},{"code":"WAVES","name":"WAVES","statuses":["primary"]},{"code":"WGC","name":"WGC","statuses":["primary"]},{"code":"XWC","name":"Whitecoin","statuses":["primary"]},{"code":"WBB","name":"Wild Beast Block","statuses":["primary"]},{"code":"WLC","name":"WLC","statuses":["primary"]},{"code":"WMC","name":"WMC","statuses":["primary"]},{"code":"LOG","name":"Woodcoin","statuses":["primary"]},{"code":"WOP","name":"WOP","statuses":["primary"]},{"code":"WDC","name":"Worldcoin","statuses":["primary"]},{"code":"XAB","name":"XAB","statuses":["primary"]},{"code":"XAI","name":"XAI","statuses":["primary"]},{"code":"XAU","name":"Xaurum","statuses":["primary"]},{"code":"XBS","name":"XBS","statuses":["primary"]},{"code":"XBU","name":"XBU","statuses":["primary"]},{"code":"XCO","name":"XCO","statuses":["primary"]},{"code":"XC","name":"XCurrency","statuses":["primary"]},{"code":"XDB","name":"XDB","statuses":["primary"]},{"code":"XEMP","name":"XEMP","statuses":["primary"]},{"code":"XFC","name":"XFC","statuses":["primary"]},{"code":"MI","name":"Xiaomicoin","statuses":["primary"]},{"code":"XID","name":"XID","statuses":["primary"]},{"code":"XJO","name":"XJO","statuses":["primary"]},{"code":"XLTCG","name":"XLTCG","statuses":["primary"]},{"code":"XMS","name":"XMS","statuses":["primary"]},{"code":"XNX","name":"XNX","statuses":["primary"]},{"code":"XPD","name":"XPD","statuses":["primary"]},{"code":"XPOKE","name":"XPOKE","statuses":["primary"]},{"code":"XPRO","name":"XPRO","statuses":["primary"]},{"code":"XQN","name":"XQN","statuses":["primary"]},{"code":"XSEED","name":"XSEED","statuses":["primary"]},{"code":"XSP","name":"XSP","statuses":["primary"]},{"code":"XT","name":"XT","statuses":["primary"]},{"code":"XTP","name":"XTP","statuses":["primary"]},{"code":"XUSD","name":"XUSD","statuses":["primary"]},{"code":"YACC","name":"YACC","statuses":["primary"]},{"code":"YAC","name":"Yacoin","statuses":["primary"]},{"code":"YAY","name":"YAY","statuses":["primary"]},{"code":"YBC","name":"Ybcoin","statuses":["primary"]},{"code":"YOC","name":"YOC","statuses":["primary"]},{"code":"YOVI","name":"YOVI","statuses":["primary"]},{"code":"YUM","name":"YUM","statuses":["primary"]},{"code":"ZCC","name":"ZCC","statuses":["primary"]},{"code":"ZEIT","name":"Zeitcoin","statuses":["primary"]},{"code":"ZET","name":"Zetacoin","statuses":["primary"]},{"code":"ZRC","name":"ZiftrCOIN","statuses":["primary"]},{"code":"ZMC","name":"ZMC","statuses":["primary"]},{"code":"ZNY","name":"ZNY","statuses":["primary"]},{"code":"ZS","name":"ZS","statuses":["primary"]}]} \ No newline at end of file diff --git a/app/fonts/DIN Next/DIN Next W01 Bold.otf b/app/fonts/DIN Next/DIN Next W01 Bold.otf new file mode 100644 index 000000000..2b78d1ff4 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W01 Bold.otf differ diff --git a/app/fonts/DIN Next/DIN Next W01 Regular.otf b/app/fonts/DIN Next/DIN Next W01 Regular.otf new file mode 100644 index 000000000..09f6ee297 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W01 Regular.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Black.otf b/app/fonts/DIN Next/DIN Next W10 Black.otf new file mode 100644 index 000000000..08eb73373 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Black.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Italic.otf b/app/fonts/DIN Next/DIN Next W10 Italic.otf new file mode 100644 index 000000000..73f2b9e8c Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Italic.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Light.otf b/app/fonts/DIN Next/DIN Next W10 Light.otf new file mode 100644 index 000000000..700450e49 Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Light.otf differ diff --git a/app/fonts/DIN Next/DIN Next W10 Medium.otf b/app/fonts/DIN Next/DIN Next W10 Medium.otf new file mode 100644 index 000000000..b73f2e43f Binary files /dev/null and b/app/fonts/DIN Next/DIN Next W10 Medium.otf differ diff --git a/app/fonts/DIN_OT/DINOT-2.otf b/app/fonts/DIN_OT/DINOT-2.otf new file mode 100644 index 000000000..4a5e13127 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-Bold 2.otf b/app/fonts/DIN_OT/DINOT-Bold 2.otf new file mode 100644 index 000000000..6ed5b6c3d Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-Bold 2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-BoldItalic.otf b/app/fonts/DIN_OT/DINOT-BoldItalic.otf new file mode 100644 index 000000000..148c90588 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-BoldItalic.otf differ diff --git a/app/fonts/DIN_OT/DINOT-Italic 2.otf b/app/fonts/DIN_OT/DINOT-Italic 2.otf new file mode 100644 index 000000000..e365e77ab Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-Italic 2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-Medium 2.otf b/app/fonts/DIN_OT/DINOT-Medium 2.otf new file mode 100644 index 000000000..a87a2df37 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-Medium 2.otf differ diff --git a/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf b/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf new file mode 100644 index 000000000..14eddfc76 Binary files /dev/null and b/app/fonts/DIN_OT/DINOT-MediumItalic 2.otf differ diff --git a/app/fonts/Lato/Lato-Black.ttf b/app/fonts/Lato/Lato-Black.ttf new file mode 100755 index 000000000..6848db0d1 Binary files /dev/null and b/app/fonts/Lato/Lato-Black.ttf differ diff --git a/app/fonts/Lato/Lato-BlackItalic.ttf b/app/fonts/Lato/Lato-BlackItalic.ttf new file mode 100755 index 000000000..5decf1297 Binary files /dev/null and b/app/fonts/Lato/Lato-BlackItalic.ttf differ diff --git a/app/fonts/Lato/Lato-Bold.ttf b/app/fonts/Lato/Lato-Bold.ttf new file mode 100755 index 000000000..74343694e Binary files /dev/null and b/app/fonts/Lato/Lato-Bold.ttf differ diff --git a/app/fonts/Lato/Lato-BoldItalic.ttf b/app/fonts/Lato/Lato-BoldItalic.ttf new file mode 100755 index 000000000..684aacf5b Binary files /dev/null and b/app/fonts/Lato/Lato-BoldItalic.ttf differ diff --git a/app/fonts/Lato/Lato-Hairline.ttf b/app/fonts/Lato/Lato-Hairline.ttf new file mode 100755 index 000000000..288be2955 Binary files /dev/null and b/app/fonts/Lato/Lato-Hairline.ttf differ diff --git a/app/fonts/Lato/Lato-HairlineItalic.ttf b/app/fonts/Lato/Lato-HairlineItalic.ttf new file mode 100755 index 000000000..c2bfd3353 Binary files /dev/null and b/app/fonts/Lato/Lato-HairlineItalic.ttf differ diff --git a/app/fonts/Lato/Lato-Italic.ttf b/app/fonts/Lato/Lato-Italic.ttf new file mode 100755 index 000000000..3d3b7a298 Binary files /dev/null and b/app/fonts/Lato/Lato-Italic.ttf differ diff --git a/app/fonts/Lato/Lato-Light.ttf b/app/fonts/Lato/Lato-Light.ttf new file mode 100755 index 000000000..a958067a8 Binary files /dev/null and b/app/fonts/Lato/Lato-Light.ttf differ diff --git a/app/fonts/Lato/Lato-LightItalic.ttf b/app/fonts/Lato/Lato-LightItalic.ttf new file mode 100755 index 000000000..5e45ad9a6 Binary files /dev/null and b/app/fonts/Lato/Lato-LightItalic.ttf differ diff --git a/app/fonts/Lato/Lato-Regular.ttf b/app/fonts/Lato/Lato-Regular.ttf new file mode 100755 index 000000000..04ea8efb1 Binary files /dev/null and b/app/fonts/Lato/Lato-Regular.ttf differ diff --git a/app/fonts/Lato/OFL.txt b/app/fonts/Lato/OFL.txt new file mode 100755 index 000000000..dfca0da4b --- /dev/null +++ b/app/fonts/Lato/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/fonts/Roboto/Roboto-Black.ttf b/app/fonts/Roboto/Roboto-Black.ttf new file mode 100644 index 000000000..71f01ac2b Binary files /dev/null and b/app/fonts/Roboto/Roboto-Black.ttf differ diff --git a/app/fonts/Roboto/Roboto-BlackItalic.ttf b/app/fonts/Roboto/Roboto-BlackItalic.ttf new file mode 100644 index 000000000..ec309c785 Binary files /dev/null and b/app/fonts/Roboto/Roboto-BlackItalic.ttf differ diff --git a/app/fonts/Roboto/Roboto-Bold.ttf b/app/fonts/Roboto/Roboto-Bold.ttf new file mode 100644 index 000000000..aaf374d2c Binary files /dev/null and b/app/fonts/Roboto/Roboto-Bold.ttf differ diff --git a/app/fonts/Roboto/Roboto-BoldItalic.ttf b/app/fonts/Roboto/Roboto-BoldItalic.ttf new file mode 100644 index 000000000..dcd0f8007 Binary files /dev/null and b/app/fonts/Roboto/Roboto-BoldItalic.ttf differ diff --git a/app/fonts/Roboto/Roboto-Italic.ttf b/app/fonts/Roboto/Roboto-Italic.ttf new file mode 100644 index 000000000..f382c6874 Binary files /dev/null and b/app/fonts/Roboto/Roboto-Italic.ttf differ diff --git a/app/fonts/Roboto/Roboto-Light.ttf b/app/fonts/Roboto/Roboto-Light.ttf new file mode 100644 index 000000000..664e1b2f9 Binary files /dev/null and b/app/fonts/Roboto/Roboto-Light.ttf differ diff --git a/app/fonts/Roboto/Roboto-LightItalic.ttf b/app/fonts/Roboto/Roboto-LightItalic.ttf new file mode 100644 index 000000000..b8f529637 Binary files /dev/null and b/app/fonts/Roboto/Roboto-LightItalic.ttf differ diff --git a/app/fonts/Roboto/Roboto-Medium.ttf b/app/fonts/Roboto/Roboto-Medium.ttf new file mode 100644 index 000000000..aa00de0ef Binary files /dev/null and b/app/fonts/Roboto/Roboto-Medium.ttf differ diff --git a/app/fonts/Roboto/Roboto-MediumItalic.ttf b/app/fonts/Roboto/Roboto-MediumItalic.ttf new file mode 100644 index 000000000..67e25f019 Binary files /dev/null and b/app/fonts/Roboto/Roboto-MediumItalic.ttf differ diff --git a/app/fonts/Roboto/Roboto-Regular.ttf b/app/fonts/Roboto/Roboto-Regular.ttf new file mode 100644 index 000000000..3e6e2e761 Binary files /dev/null and b/app/fonts/Roboto/Roboto-Regular.ttf differ diff --git a/app/fonts/Roboto/Roboto-Thin.ttf b/app/fonts/Roboto/Roboto-Thin.ttf new file mode 100644 index 000000000..d262d1446 Binary files /dev/null and b/app/fonts/Roboto/Roboto-Thin.ttf differ diff --git a/app/fonts/Roboto/Roboto-ThinItalic.ttf b/app/fonts/Roboto/Roboto-ThinItalic.ttf new file mode 100644 index 000000000..63e9f9718 Binary files /dev/null and b/app/fonts/Roboto/Roboto-ThinItalic.ttf differ diff --git a/app/fonts/Roboto/RobotoCondensed-Bold.ttf b/app/fonts/Roboto/RobotoCondensed-Bold.ttf new file mode 100644 index 000000000..48dd63534 Binary files /dev/null and b/app/fonts/Roboto/RobotoCondensed-Bold.ttf differ diff --git a/app/fonts/Roboto/RobotoCondensed-BoldItalic.ttf b/app/fonts/Roboto/RobotoCondensed-BoldItalic.ttf new file mode 100644 index 000000000..ad728646a Binary files /dev/null and b/app/fonts/Roboto/RobotoCondensed-BoldItalic.ttf differ diff --git a/app/fonts/Roboto/RobotoCondensed-Italic.ttf b/app/fonts/Roboto/RobotoCondensed-Italic.ttf new file mode 100644 index 000000000..a232513d5 Binary files /dev/null and b/app/fonts/Roboto/RobotoCondensed-Italic.ttf differ diff --git a/app/fonts/Roboto/RobotoCondensed-Light.ttf b/app/fonts/Roboto/RobotoCondensed-Light.ttf new file mode 100644 index 000000000..a6e368d40 Binary files /dev/null and b/app/fonts/Roboto/RobotoCondensed-Light.ttf differ diff --git a/app/fonts/Roboto/RobotoCondensed-LightItalic.ttf b/app/fonts/Roboto/RobotoCondensed-LightItalic.ttf new file mode 100644 index 000000000..5b2b6ae08 Binary files /dev/null and b/app/fonts/Roboto/RobotoCondensed-LightItalic.ttf differ diff --git a/app/fonts/Roboto/RobotoCondensed-Regular.ttf b/app/fonts/Roboto/RobotoCondensed-Regular.ttf new file mode 100644 index 000000000..65bf32a19 Binary files /dev/null and b/app/fonts/Roboto/RobotoCondensed-Regular.ttf differ diff --git a/app/home.html b/app/home.html new file mode 100644 index 000000000..cfb4b00a0 --- /dev/null +++ b/app/home.html @@ -0,0 +1,12 @@ + + + + + + MetaMask Plugin + + +
+ + + diff --git a/app/images/.DS_Store b/app/images/.DS_Store deleted file mode 100644 index d28ef2089..000000000 Binary files a/app/images/.DS_Store and /dev/null differ diff --git a/app/images/caret-right.svg b/app/images/caret-right.svg new file mode 100644 index 000000000..8981ac254 --- /dev/null +++ b/app/images/caret-right.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/check-white.svg b/app/images/check-white.svg new file mode 100644 index 000000000..0f15667da --- /dev/null +++ b/app/images/check-white.svg @@ -0,0 +1,14 @@ + + + + check-white + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/app/images/coinbase logo.png b/app/images/coinbase logo.png new file mode 100644 index 000000000..a23d7926d Binary files /dev/null and b/app/images/coinbase logo.png differ diff --git a/app/images/eth_logo.svg b/app/images/eth_logo.svg new file mode 100644 index 000000000..894bd70dd --- /dev/null +++ b/app/images/eth_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/import-account.svg b/app/images/import-account.svg new file mode 100644 index 000000000..d6a81b70c --- /dev/null +++ b/app/images/import-account.svg @@ -0,0 +1,18 @@ + + + + import-account + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/info-logo.png b/app/images/info-logo.png new file mode 100644 index 000000000..f654ed5b1 Binary files /dev/null and b/app/images/info-logo.png differ diff --git a/app/images/metamask-fox.svg b/app/images/metamask-fox.svg new file mode 100644 index 000000000..f3c24f79e --- /dev/null +++ b/app/images/metamask-fox.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/mm-bolt.svg b/app/images/mm-bolt.svg new file mode 100644 index 000000000..bbf0abcc7 --- /dev/null +++ b/app/images/mm-bolt.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/images/mm-info-icon.svg b/app/images/mm-info-icon.svg new file mode 100644 index 000000000..825f0f200 --- /dev/null +++ b/app/images/mm-info-icon.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/images/open.svg b/app/images/open.svg new file mode 100644 index 000000000..2957ce43d --- /dev/null +++ b/app/images/open.svg @@ -0,0 +1,15 @@ + + + + open + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/app/images/plus-btn-white.svg b/app/images/plus-btn-white.svg new file mode 100644 index 000000000..2672d39dd --- /dev/null +++ b/app/images/plus-btn-white.svg @@ -0,0 +1,17 @@ + + + + plus-btn-white + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/popout.svg b/app/images/popout.svg new file mode 100644 index 000000000..760fe4379 --- /dev/null +++ b/app/images/popout.svg @@ -0,0 +1,21 @@ + + + + popout + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/settings.svg b/app/images/settings.svg index fe61320a5..cf9b298dd 100644 --- a/app/images/settings.svg +++ b/app/images/settings.svg @@ -1,24 +1,22 @@ - - - - - - + + + + settings + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/shapeshift logo.png b/app/images/shapeshift logo.png new file mode 100644 index 000000000..ac8faba5b Binary files /dev/null and b/app/images/shapeshift logo.png differ diff --git a/app/manifest.json b/app/manifest.json index a3242149b..eab6c7063 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.5.2", + "version": "4.1.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", @@ -58,8 +58,8 @@ "storage", "clipboardWrite", "http://localhost:8545/", - "https://www.cryptonator.com/" - ], + "https://*.infura.io/" + ], "web_accessible_resources": [ "scripts/inpage.js" ], diff --git a/app/notification.html b/app/notification.html index cc485da7f..f10cbbf41 100644 --- a/app/notification.html +++ b/app/notification.html @@ -1,5 +1,5 @@ - + MetaMask Notification @@ -9,7 +9,7 @@ } - +
diff --git a/app/popup.html b/app/popup.html index 6d85a9811..bf09b97ca 100644 --- a/app/popup.html +++ b/app/popup.html @@ -1,11 +1,12 @@ - + + MetaMask Plugin - +
- \ No newline at end of file + diff --git a/app/scripts/account-import-strategies/index.js b/app/scripts/account-import-strategies/index.js index d5124eb7f..96e2b5912 100644 --- a/app/scripts/account-import-strategies/index.js +++ b/app/scripts/account-import-strategies/index.js @@ -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) diff --git a/app/scripts/background.js b/app/scripts/background.js index 7211f1e0c..476d073d1 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,22 +1,24 @@ const urlUtil = require('url') const endOfStream = require('end-of-stream') -const asyncQ = require('async-q') -const pipe = require('pump') +const pump = require('pump') +const log = require('loglevel') +const extension = require('extensionizer') const LocalStorageStore = require('obs-store/lib/localStorage') const storeTransform = require('obs-store/lib/transform') +const asStream = require('obs-store/lib/asStream') const ExtensionPlatform = require('./platforms/extension') const Migrator = require('./lib/migrator/') const migrations = require('./migrations/') const PortStream = require('./lib/port-stream.js') const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') -const extension = require('extensionizer') const firstTimeState = require('./first-time-state') +const setupRaven = require('./setupRaven') +const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' -const log = require('loglevel') window.log = log log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') @@ -24,44 +26,46 @@ const platform = new ExtensionPlatform() const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager +// setup sentry error reporting +const release = platform.getVersion() +const raven = setupRaven({ release }) + let popupIsOpen = false +let openMetamaskTabsIDs = {} // state persistence const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) // initialization flow -asyncQ.waterfall([ - () => loadStateFromPersistence(), - (initState) => setupController(initState), -]) -.then(() => console.log('MetaMask initialization complete.')) -.catch((err) => { console.error(err) }) +initialize().catch(log.error) + +// setup metamask mesh testing container +setupMetamaskMeshMetrics() + +async function initialize () { + const initState = await loadStateFromPersistence() + await setupController(initState) + log.debug('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 // @@ -78,15 +82,26 @@ function setupController (initState) { }) global.metamaskController = controller + // report failed transactions to Sentry + controller.txController.on(`tx:status-update`, (txId, status) => { + if (status !== 'failed') return + const txMeta = controller.txController.txStateManager.getTx(txId) + const errorMessage = `Transaction Failed: ${txMeta.err.message}` + raven.captureMessage(errorMessage, { + // "extra" key is required by Sentry + extra: txMeta, + }) + }) + // setup state persistence - pipe( - controller.store, + pump( + asStream(controller.store), storeTransform(versionifyData), - diskStore + asStream(diskStore) ) - function versionifyData(state) { - let versionedData = diskStore.getState() + function versionifyData (state) { + const versionedData = diskStore.getState() versionedData.data = state return versionedData } @@ -97,21 +112,27 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' - var portStream = new PortStream(remotePort) + const isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' + const portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { // communication with popup popupIsOpen = popupIsOpen || (remotePort.name === 'popup') - controller.setupTrustedCommunication(portStream, 'MetaMask', remotePort.name) + controller.setupTrustedCommunication(portStream, 'MetaMask') // record popup as closed + if (remotePort.sender.url.match(/home.html$/)) { + openMetamaskTabsIDs[remotePort.sender.tab.id] = true + } if (remotePort.name === 'popup') { endOfStream(portStream, () => { popupIsOpen = false + if (remotePort.sender.url.match(/home.html$/)) { + openMetamaskTabsIDs[remotePort.sender.tab.id] = false + } }) } } else { // communication with page - var originDomain = urlUtil.parse(remotePort.sender.url).hostname + const originDomain = urlUtil.parse(remotePort.sender.url).hostname controller.setupUntrustedCommunication(portStream, originDomain) } } @@ -121,15 +142,18 @@ function setupController (initState) { // updateBadge() - controller.txManager.on('updateBadge', updateBadge) + controller.txController.on('update:badge', updateBadge) controller.messageManager.on('updateBadge', updateBadge) + controller.personalMessageManager.on('updateBadge', updateBadge) // plugin badge text function updateBadge () { var label = '' - var unapprovedTxCount = controller.txManager.unapprovedTxCount + var unapprovedTxCount = controller.txController.getUnapprovedTxCount() var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount - var count = unapprovedTxCount + unapprovedMsgCount + var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount + var unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount + var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs if (count) { label = String(count) } @@ -138,7 +162,6 @@ function setupController (initState) { } return Promise.resolve() - } // @@ -147,7 +170,10 @@ function setupController (initState) { // popup trigger function triggerUi () { - if (!popupIsOpen) notificationManager.showPopup() + extension.tabs.query({ active: true }, (tabs) => { + const currentlyActiveMetamaskTab = tabs.find(tab => openMetamaskTabsIDs[tab.id]) + if (!popupIsOpen && !currentlyActiveMetamaskTab) notificationManager.showPopup() + }) } // On first install, open a window to MetaMask website to how-it-works. diff --git a/app/scripts/config.js b/app/scripts/config.js index ec421744d..74c5b576e 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -1,16 +1,44 @@ 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' +const LOCALHOST_RPC_URL = 'http://localhost:8545' + +const MAINET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2' +const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2' +const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2' +const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2' + +const DEFAULT_RPC = 'rinkeby' +const OLD_UI_NETWORK_TYPE = 'network' +const BETA_UI_NETWORK_TYPE = 'networkBeta' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' module.exports = { network: { - default: DEFAULT_RPC_URL, + localhost: LOCALHOST_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, + }, + // Used for beta UI + networkBeta: { + localhost: LOCALHOST_RPC_URL, + mainnet: MAINET_RPC_URL_BETA, + ropsten: ROPSTEN_RPC_URL_BETA, + kovan: KOVAN_RPC_URL_BETA, + rinkeby: RINKEBY_RPC_URL_BETA, + }, + networkNames: { + 3: 'Ropsten', + 4: 'Rinkeby', + 42: 'Kovan', + }, + enums: { + DEFAULT_RPC, + OLD_UI_NETWORK_TYPE, + BETA_UI_NETWORK_TYPE, }, } diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 4d7e682d3..2ed7c87b6 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,12 +1,15 @@ -const LocalMessageDuplexStream = require('post-message-stream') -const PongStream = require('ping-pong-stream/pong') -const PortStream = require('./lib/port-stream.js') -const ObjectMultiplex = require('./lib/obj-multiplex') -const extension = require('extensionizer') - const fs = require('fs') const path = require('path') -const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString() +const pump = require('pump') +const LocalMessageDuplexStream = require('post-message-stream') +const PongStream = require('ping-pong-stream/pong') +const ObjectMultiplex = require('obj-multiplex') +const extension = require('extensionizer') +const PortStream = require('./lib/port-stream.js') + +const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString() +const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n' +const inpageBundle = inpageContent + inpageSuffix // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -24,8 +27,7 @@ function setupInjection () { try { // inject in-page script var scriptTag = document.createElement('script') - scriptTag.src = extension.extension.getURL('scripts/inpage.js') - scriptTag.textContent = inpageText + scriptTag.textContent = inpageBundle scriptTag.onload = function () { this.parentNode.removeChild(this) } var container = document.head || document.documentElement // append as first child @@ -37,35 +39,64 @@ function setupInjection () { function setupStreams () { // setup communication to page and plugin - var pageStream = new LocalMessageDuplexStream({ + const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) - pageStream.on('error', console.error) - var pluginPort = extension.runtime.connect({name: 'contentscript'}) - var pluginStream = new PortStream(pluginPort) - pluginStream.on('error', console.error) + const pluginPort = extension.runtime.connect({ name: 'contentscript' }) + const pluginStream = new PortStream(pluginPort) // forward communication plugin->inpage - pageStream.pipe(pluginStream).pipe(pageStream) + pump( + pageStream, + pluginStream, + pageStream, + (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) + ) // setup local multistream channels - var mx = ObjectMultiplex() - mx.on('error', console.error) - mx.pipe(pageStream).pipe(mx) + const mux = new ObjectMultiplex() + mux.setMaxListeners(25) + + pump( + mux, + pageStream, + mux, + (err) => logStreamDisconnectWarning('MetaMask Inpage', err) + ) + pump( + mux, + pluginStream, + mux, + (err) => logStreamDisconnectWarning('MetaMask Background', err) + ) // connect ping stream - var pongStream = new PongStream({ objectMode: true }) - pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream) + const pongStream = new PongStream({ objectMode: true }) + pump( + mux, + pongStream, + mux, + (err) => logStreamDisconnectWarning('MetaMask PingPongStream', err) + ) - // ignore unused channels (handled by background) - mx.ignoreStream('provider') - mx.ignoreStream('publicConfig') - mx.ignoreStream('reload') + // connect phishing warning stream + const phishingStream = mux.createStream('phishing') + phishingStream.once('data', redirectToPhishingWarning) + + // ignore unused channels (handled by background, inpage) + mux.ignoreStream('provider') + mux.ignoreStream('publicConfig') +} + +function logStreamDisconnectWarning (remoteLabel, err) { + let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}` + if (err) warningMsg += '\n' + err.stack + console.warn(warningMsg) } function shouldInjectWeb3 () { - return doctypeCheck() || suffixCheck() + return doctypeCheck() && suffixCheck() && documentElementCheck() } function doctypeCheck () { @@ -73,19 +104,32 @@ function doctypeCheck () { if (doctype) { return doctype.name === 'html' } else { - return false + return true } } -function suffixCheck() { +function suffixCheck () { var prohibitedTypes = ['xml', 'pdf'] var currentUrl = window.location.href var currentRegex for (let i = 0; i < prohibitedTypes.length; i++) { - currentRegex = new RegExp(`\.${prohibitedTypes[i]}$`) + currentRegex = new RegExp(`\\.${prohibitedTypes[i]}$`) if (currentRegex.test(currentUrl)) { return false } } return true } + +function documentElementCheck () { + var documentElement = document.documentElement.nodeName + if (documentElement) { + return documentElement.toLowerCase() === 'html' + } + return true +} + +function redirectToPhishingWarning () { + console.log('MetaMask - redirecting to phishing warning') + window.location.href = 'https://metamask.io/phishing.html' +} diff --git a/app/scripts/controllers/address-book.js b/app/scripts/controllers/address-book.js index c66eb2bd4..6fb4ee114 100644 --- a/app/scripts/controllers/address-book.js +++ b/app/scripts/controllers/address-book.js @@ -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) diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js new file mode 100644 index 000000000..f83f294cc --- /dev/null +++ b/app/scripts/controllers/balance.js @@ -0,0 +1,80 @@ +const ObservableStore = require('obs-store') +const PendingBalanceCalculator = require('../lib/pending-balance-calculator') +const BN = require('ethereumjs-util').BN + +class BalanceController { + + constructor (opts = {}) { + this._validateParams(opts) + const { address, accountTracker, txController, blockTracker } = opts + + this.address = address + this.accountTracker = accountTracker + this.txController = txController + this.blockTracker = blockTracker + + const initState = { + ethBalance: undefined, + } + this.store = new ObservableStore(initState) + + this.balanceCalc = new PendingBalanceCalculator({ + getBalance: () => this._getBalance(), + getPendingTransactions: this._getPendingTransactions.bind(this), + }) + + this._registerUpdates() + } + + async updateBalance () { + const balance = await this.balanceCalc.getBalance() + this.store.updateState({ + ethBalance: balance, + }) + } + + _registerUpdates () { + const update = this.updateBalance.bind(this) + + this.txController.on('tx:status-update', (txId, status) => { + switch (status) { + case 'submitted': + case 'confirmed': + case 'failed': + update() + return + default: + return + } + }) + this.accountTracker.store.subscribe(update) + this.blockTracker.on('block', update) + } + + async _getBalance () { + const { accounts } = this.accountTracker.store.getState() + const entry = accounts[this.address] + const balance = entry.balance + return balance ? new BN(balance.substring(2), 16) : undefined + } + + async _getPendingTransactions () { + const pending = this.txController.getFilteredTxList({ + from: this.address, + status: 'submitted', + err: undefined, + }) + return pending + } + + _validateParams (opts) { + const { address, accountTracker, txController, blockTracker } = opts + if (!address || !accountTracker || !txController || !blockTracker) { + const error = 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.' + throw new Error(error) + } + } + +} + +module.exports = BalanceController diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js new file mode 100644 index 000000000..33c31dab9 --- /dev/null +++ b/app/scripts/controllers/blacklist.js @@ -0,0 +1,60 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const PhishingDetector = require('eth-phishing-detect/src/detector') + +// compute phishing lists +const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') +// every four minutes +const POLLING_INTERVAL = 4 * 60 * 1000 + +class BlacklistController { + + constructor (opts = {}) { + const initState = extend({ + phishing: PHISHING_DETECTION_CONFIG, + }, opts.initState) + this.store = new ObservableStore(initState) + // phishing detector + this._phishingDetector = null + this._setupPhishingDetector(initState.phishing) + // polling references + this._phishingUpdateIntervalRef = null + } + + // + // PUBLIC METHODS + // + + checkForPhishing (hostname) { + if (!hostname) return false + const { result } = this._phishingDetector.check(hostname) + return result + } + + async updatePhishingList () { + const response = await fetch('https://api.infura.io/v2/blacklist') + const phishing = await response.json() + this.store.updateState({ phishing }) + this._setupPhishingDetector(phishing) + return phishing + } + + scheduleUpdates () { + if (this._phishingUpdateIntervalRef) return + this.updatePhishingList() + this._phishingUpdateIntervalRef = setInterval(() => { + this.updatePhishingList() + }, POLLING_INTERVAL) + } + + // + // PRIVATE METHODS + // + + _setupPhishingDetector (config) { + this._phishingDetector = new PhishingDetector(config) + } +} + +module.exports = BlacklistController + diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js new file mode 100644 index 000000000..907b087cf --- /dev/null +++ b/app/scripts/controllers/computed-balances.js @@ -0,0 +1,77 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const BalanceController = require('./balance') + +class ComputedbalancesController { + + constructor (opts = {}) { + const { accountTracker, txController, blockTracker } = opts + this.accountTracker = accountTracker + this.txController = txController + this.blockTracker = blockTracker + + const initState = extend({ + computedBalances: {}, + }, opts.initState) + this.store = new ObservableStore(initState) + this.balances = {} + + this._initBalanceUpdating() + } + + updateAllBalances () { + Object.keys(this.balances).forEach((balance) => { + const address = balance.address + this.balances[address].updateBalance() + }) + } + + _initBalanceUpdating () { + const store = this.accountTracker.store.getState() + this.syncAllAccountsFromStore(store) + this.accountTracker.store.subscribe(this.syncAllAccountsFromStore.bind(this)) + } + + syncAllAccountsFromStore (store) { + const upstream = Object.keys(store.accounts) + const balances = Object.keys(this.balances) + .map(address => this.balances[address]) + + // Follow new addresses + for (const address in balances) { + this.trackAddressIfNotAlready(address) + } + + // Unfollow old ones + balances.forEach(({ address }) => { + if (!upstream.includes(address)) { + delete this.balances[address] + } + }) + } + + trackAddressIfNotAlready (address) { + const state = this.store.getState() + if (!(address in state.computedBalances)) { + this.trackAddress(address) + } + } + + trackAddress (address) { + const updater = new BalanceController({ + address, + accountTracker: this.accountTracker, + txController: this.txController, + blockTracker: this.blockTracker, + }) + updater.store.subscribe((accountBalance) => { + const newState = this.store.getState() + newState.computedBalances[address] = accountBalance + this.store.updateState(newState) + }) + this.balances[address] = updater + updater.updateBalance() + } +} + +module.exports = ComputedbalancesController diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index c4904f8ac..25a7a942e 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -8,7 +8,7 @@ class CurrencyController { constructor (opts = {}) { const initState = extend({ - currentCurrency: 'USD', + currentCurrency: 'usd', conversionRate: 0, conversionDate: 'N/A', }, opts.initState) @@ -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.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) .then(response => response.json()) .then((parsedResponse) => { - this.setConversionRate(Number(parsedResponse.ticker.price)) + this.setConversionRate(Number(parsedResponse.bid)) 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') + } }) } diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js new file mode 100644 index 000000000..10adb1004 --- /dev/null +++ b/app/scripts/controllers/infura.js @@ -0,0 +1,43 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +// every ten minutes +const POLLING_INTERVAL = 10 * 60 * 1000 + +class InfuraController { + + constructor (opts = {}) { + const initState = extend({ + infuraNetworkStatus: {}, + }, opts.initState) + this.store = new ObservableStore(initState) + } + + // + // PUBLIC METHODS + // + + // Responsible for retrieving the status of Infura's nodes. Can return either + // ok, degraded, or down. + checkInfuraNetworkStatus () { + return fetch('https://api.infura.io/v1/status/metamask') + .then(response => response.json()) + .then((parsedResponse) => { + this.store.updateState({ + infuraNetworkStatus: parsedResponse, + }) + return parsedResponse + }) + } + + scheduleInfuraNetworkCheck () { + if (this.conversionInterval) { + clearInterval(this.conversionInterval) + } + this.conversionInterval = setInterval(() => { + this.checkInfuraNetworkStatus() + }, POLLING_INTERVAL) + } +} + +module.exports = InfuraController diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js new file mode 100644 index 000000000..617456cd7 --- /dev/null +++ b/app/scripts/controllers/network.js @@ -0,0 +1,214 @@ +const assert = require('assert') +const EventEmitter = require('events') +const createMetamaskProvider = require('web3-provider-engine/zero.js') +const SubproviderFromProvider = require('web3-provider-engine/subproviders/web3.js') +const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider') +const ObservableStore = require('obs-store') +const ComposedStore = require('obs-store/lib/composed') +const extend = require('xtend') +const EthQuery = require('eth-query') +const createEventEmitterProxy = require('../lib/events-proxy.js') +const networkConfig = require('../config.js') +const { OLD_UI_NETWORK_TYPE, DEFAULT_RPC } = networkConfig.enums +const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet'] + +module.exports = class NetworkController extends EventEmitter { + + constructor (config) { + super() + + this._networkEndpointVersion = OLD_UI_NETWORK_TYPE + this._networkEndpoints = this.getNetworkEndpoints(OLD_UI_NETWORK_TYPE) + this._defaultRpc = this._networkEndpoints[DEFAULT_RPC] + + config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) + this.networkStore = new ObservableStore('loading') + this.providerStore = new ObservableStore(config.provider) + this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) + this._proxy = createEventEmitterProxy() + + this.on('networkDidChange', this.lookupNetwork) + } + + async setNetworkEndpoints (version) { + if (version === this._networkEndpointVersion) { + return + } + + this._networkEndpointVersion = version + this._networkEndpoints = this.getNetworkEndpoints(version) + this._defaultRpc = this._networkEndpoints[DEFAULT_RPC] + const { type } = this.getProviderConfig() + + return this.setProviderType(type, true) + } + + getNetworkEndpoints (version = OLD_UI_NETWORK_TYPE) { + return networkConfig[version] + } + + initializeProvider (_providerParams) { + this._baseProviderParams = _providerParams + const { type, rpcTarget } = this.providerStore.getState() + // map rpcTarget to rpcUrl + const opts = { + type, + rpcUrl: rpcTarget, + } + this._configureProvider(opts) + this._proxy.on('block', this._logBlock.bind(this)) + this._proxy.on('error', this.verifyNetwork.bind(this)) + this.ethQuery = new EthQuery(this._proxy) + this.lookupNetwork() + return this._proxy + } + + 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 () { + // Prevent firing when provider is not defined. + if (!this.ethQuery || !this.ethQuery.sendAsync) { + return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery') + } + 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, + }) + this._switchNetwork({ rpcUrl }) + } + + getCurrentRpcAddress () { + const provider = this.getProviderConfig() + if (!provider) return null + return this.getRpcAddressForType(provider.type) + } + + async setProviderType (type, forceUpdate = false) { + assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`) + // skip if type already matches + if (type === this.getProviderConfig().type && !forceUpdate) { + return + } + + const rpcTarget = this.getRpcAddressForType(type) + assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) + this.providerStore.updateState({ type, rpcTarget }) + this._switchNetwork({ type }) + } + + getProviderConfig () { + return this.providerStore.getState() + } + + getRpcAddressForType (type, provider = this.getProviderConfig()) { + if (this._networkEndpoints[type]) { + return this._networkEndpoints[type] + } + + return provider && provider.rpcTarget ? provider.rpcTarget : this._defaultRpc + } + + // + // Private + // + + _switchNetwork (opts) { + this.setNetworkState('loading') + this._configureProvider(opts) + this.emit('networkDidChange') + } + + _configureProvider (opts) { + // type-based rpc endpoints + const { type } = opts + if (type) { + // type-based infura rpc endpoints + const isInfura = INFURA_PROVIDER_TYPES.includes(type) + opts.rpcUrl = this.getRpcAddressForType(type) + if (isInfura) { + this._configureInfuraProvider(opts) + // other type-based rpc endpoints + } else { + this._configureStandardProvider(opts) + } + // url-based rpc endpoints + } else { + this._configureStandardProvider(opts) + } + } + + _configureInfuraProvider (opts) { + log.info('_configureInfuraProvider', opts) + const infuraProvider = createInfuraProvider({ + network: opts.type, + }) + const infuraSubprovider = new SubproviderFromProvider(infuraProvider) + const providerParams = extend(this._baseProviderParams, { + rpcUrl: opts.rpcUrl, + engineParams: { + pollingInterval: 8000, + blockTrackerProvider: infuraProvider, + }, + dataSubprovider: infuraSubprovider, + }) + const provider = createMetamaskProvider(providerParams) + this._setProvider(provider) + } + + _configureStandardProvider ({ rpcUrl }) { + const providerParams = extend(this._baseProviderParams, { + rpcUrl, + engineParams: { + pollingInterval: 8000, + }, + }) + const provider = createMetamaskProvider(providerParams) + this._setProvider(provider) + } + + _setProvider (provider) { + // collect old block tracker events + const oldProvider = this._provider + let blockTrackerHandlers + if (oldProvider) { + // capture old block handlers + blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers + // tear down + oldProvider.removeAllListeners() + oldProvider.stop() + } + // override block tracler + provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) + // set as new provider + this._provider = provider + this._proxy.setTarget(provider) + } + + _logBlock (block) { + log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) + this.verifyNetwork() + } +} diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index c7f675a41..39d15fd83 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -7,13 +7,22 @@ class PreferencesController { constructor (opts = {}) { const initState = extend({ frequentRpcList: [], + currentAccountTab: 'history', + tokens: [], + useBlockie: false, + featureFlags: {}, }, opts.initState) this.store = new ObservableStore(initState) } +// PUBLIC METHODS - // - // PUBLIC METHODS - // + setUseBlockie (val) { + this.store.updateState({ useBlockie: val }) + } + + getUseBlockie () { + return this.store.getState().useBlockie + } setSelectedAddress (_address) { return new Promise((resolve, reject) => { @@ -23,10 +32,44 @@ class PreferencesController { }) } - getSelectedAddress (_address) { + getSelectedAddress () { return this.store.getState().selectedAddress } + async addToken (rawAddress, symbol, decimals) { + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals } + + const tokens = this.store.getState().tokens + const previousEntry = tokens.find((token, index) => { + return token.address === address + }) + const previousIndex = tokens.indexOf(previousEntry) + + if (previousEntry) { + tokens[previousIndex] = newEntry + } else { + tokens.push(newEntry) + } + + this.store.updateState({ tokens }) + + return Promise.resolve(tokens) + } + + removeToken (rawAddress) { + const tokens = this.store.getState().tokens + + const updatedTokens = tokens.filter(token => token.address !== rawAddress) + + this.store.updateState({ tokens: updatedTokens }) + return Promise.resolve(updatedTokens) + } + + getTokens () { + return this.store.getState().tokens + } + updateFrequentRpcList (_url) { return this.addToFrequentRpcList(_url) .then((rpcList) => { @@ -35,9 +78,16 @@ class PreferencesController { }) } + setCurrentAccountTab (currentAccountTab) { + return new Promise((resolve, reject) => { + this.store.updateState({ currentAccountTab }) + resolve() + }) + } + 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) } @@ -54,12 +104,24 @@ class PreferencesController { return this.store.getState().frequentRpcList } + setFeatureFlag (feature, activated) { + const currentFeatureFlags = this.store.getState().featureFlags + const updatedFeatureFlags = { + ...currentFeatureFlags, + [feature]: activated, + } + + this.store.updateState({ featureFlags: updatedFeatureFlags }) + + return Promise.resolve(updatedFeatureFlags) + } + + getFeatureFlags () { + return this.store.getState().featureFlags + } // // PRIVATE METHODS // - - - } module.exports = PreferencesController diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js new file mode 100644 index 000000000..4ae3810eb --- /dev/null +++ b/app/scripts/controllers/recent-blocks.js @@ -0,0 +1,110 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const BN = require('ethereumjs-util').BN +const EthQuery = require('eth-query') + +class RecentBlocksController { + + constructor (opts = {}) { + const { blockTracker, provider } = opts + this.blockTracker = blockTracker + this.ethQuery = new EthQuery(provider) + this.historyLength = opts.historyLength || 40 + + const initState = extend({ + recentBlocks: [], + }, opts.initState) + this.store = new ObservableStore(initState) + + this.blockTracker.on('block', this.processBlock.bind(this)) + this.backfill() + } + + resetState () { + this.store.updateState({ + recentBlocks: [], + }) + } + + processBlock (newBlock) { + const block = this.mapTransactionsToPrices(newBlock) + + const state = this.store.getState() + state.recentBlocks.push(block) + + while (state.recentBlocks.length > this.historyLength) { + state.recentBlocks.shift() + } + + this.store.updateState(state) + } + + backfillBlock (newBlock) { + const block = this.mapTransactionsToPrices(newBlock) + + const state = this.store.getState() + + if (state.recentBlocks.length < this.historyLength) { + state.recentBlocks.unshift(block) + } + + this.store.updateState(state) + } + + mapTransactionsToPrices (newBlock) { + const block = extend(newBlock, { + gasPrices: newBlock.transactions.map((tx) => { + return tx.gasPrice + }), + }) + delete block.transactions + return block + } + + async backfill() { + this.blockTracker.once('block', async (block) => { + let blockNum = block.number + let recentBlocks + let state = this.store.getState() + recentBlocks = state.recentBlocks + + while (recentBlocks.length < this.historyLength) { + try { + let blockNumBn = new BN(blockNum.substr(2), 16) + const newNum = blockNumBn.subn(1).toString(10) + const newBlock = await this.getBlockByNumber(newNum) + + if (newBlock) { + this.backfillBlock(newBlock) + blockNum = newBlock.number + } + + state = this.store.getState() + recentBlocks = state.recentBlocks + } catch (e) { + log.error(e) + } + await this.wait() + } + }) + } + + async wait () { + return new Promise((resolve) => { + setTimeout(resolve, 100) + }) + } + + async getBlockByNumber (number) { + const bn = new BN(number) + return new Promise((resolve, reject) => { + this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => { + if (err) reject(err) + resolve(block) + }) + }) + } + +} + +module.exports = RecentBlocksController diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js new file mode 100644 index 000000000..ef5578d5a --- /dev/null +++ b/app/scripts/controllers/transactions.js @@ -0,0 +1,325 @@ +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const Transaction = require('ethereumjs-tx') +const EthQuery = require('ethjs-query') +const TransactionStateManger = require('../lib/tx-state-manager') +const TxGasUtil = require('../lib/tx-gas-utils') +const PendingTransactionTracker = require('../lib/pending-tx-tracker') +const createId = require('../lib/random-id') +const NonceTracker = require('../lib/nonce-tracker') + +/* + Transaction Controller is an aggregate of sub-controllers and trackers + composing them in a way to be exposed to the metamask controller + - txStateManager + responsible for the state of a transaction and + storing the transaction + - pendingTxTracker + watching blocks for transactions to be include + and emitting confirmed events + - txGasUtil + gas calculations and safety buffering + - nonceTracker + calculating nonces +*/ + +module.exports = class TransactionController extends EventEmitter { + constructor (opts) { + super() + this.networkStore = opts.networkStore || new ObservableStore({}) + this.preferencesStore = opts.preferencesStore || new ObservableStore({}) + this.provider = opts.provider + this.blockTracker = opts.blockTracker + this.signEthTx = opts.signTransaction + this.getGasPrice = opts.getGasPrice + + this.memStore = new ObservableStore({}) + this.query = new EthQuery(this.provider) + this.txGasUtil = new TxGasUtil(this.provider) + + this.txStateManager = new TransactionStateManger({ + initState: opts.initState, + txHistoryLimit: opts.txHistoryLimit, + getNetwork: this.getNetwork.bind(this), + }) + + this.txStateManager.getFilteredTxList({ + status: 'unapproved', + loadingDefaults: true, + }).forEach((tx) => { + this.addTxDefaults(tx) + .then((txMeta) => { + txMeta.loadingDefaults = false + this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') + }).catch((error) => { + this.txStateManager.setTxStatusFailed(tx.id, error) + }) + }) + + this.txStateManager.getFilteredTxList({ + status: 'approved', + }).forEach((txMeta) => { + const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') + this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) + }) + + + this.store = this.txStateManager.store + this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) + this.nonceTracker = new NonceTracker({ + provider: this.provider, + getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), + getConfirmedTransactions: (address) => { + return this.txStateManager.getFilteredTxList({ + from: address, + status: 'confirmed', + err: undefined, + }) + }, + }) + + this.pendingTxTracker = new PendingTransactionTracker({ + provider: this.provider, + nonceTracker: this.nonceTracker, + publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx), + getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), + getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), + }) + + this.txStateManager.store.subscribe(() => this.emit('update:badge')) + + this.pendingTxTracker.on('tx:warning', (txMeta) => { + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') + }) + this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { + if (!txMeta.firstRetryBlockNumber) { + txMeta.firstRetryBlockNumber = latestBlockNumber + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update') + } + }) + this.pendingTxTracker.on('tx:retry', (txMeta) => { + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + txMeta.retryCount++ + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') + }) + + this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) + // this is a little messy but until ethstore has been either + // removed or redone this is to guard against the race condition + this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) + this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) + // memstore is computed from a few different stores + this._updateMemstore() + this.txStateManager.store.subscribe(() => this._updateMemstore()) + this.networkStore.subscribe(() => this._updateMemstore()) + this.preferencesStore.subscribe(() => this._updateMemstore()) + } + + getState () { + return this.memStore.getState() + } + + getNetwork () { + return this.networkStore.getState() + } + + getSelectedAddress () { + return this.preferencesStore.getState().selectedAddress + } + + getUnapprovedTxCount () { + return Object.keys(this.txStateManager.getUnapprovedTxList()).length + } + + getPendingTxCount (account) { + return this.txStateManager.getPendingTransactions(account).length + } + + getFilteredTxList (opts) { + return this.txStateManager.getFilteredTxList(opts) + } + + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } + } + + wipeTransactions (address) { + this.txStateManager.wipeTransactions(address) + } + + // Adds a tx to the txlist + addTx (txMeta) { + this.txStateManager.addTx(txMeta) + this.emit(`${txMeta.id}:unapproved`, txMeta) + } + + async newUnapprovedTransaction (txParams) { + log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) + const initialTxMeta = await this.addUnapprovedTransaction(txParams) + // listen for tx completion (success, fail) + return new Promise((resolve, reject) => { + this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => { + switch (finishedTxMeta.status) { + case 'submitted': + return resolve(finishedTxMeta.hash) + case 'rejected': + return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) + case 'failed': + return reject(new Error(finishedTxMeta.err.message)) + default: + return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)) + } + }) + }) + } + + async addUnapprovedTransaction (txParams) { + // validate + await this.txGasUtil.validateTxParams(txParams) + // construct txMeta + const txMeta = { + id: createId(), + time: (new Date()).getTime(), + status: 'unapproved', + metamaskNetworkId: this.getNetwork(), + txParams: txParams, + loadingDefaults: true, + } + this.addTx(txMeta) + this.emit('newUnapprovedTx', txMeta) + // add default tx params + try { + await this.addTxDefaults(txMeta) + } catch (error) { + console.log(error) + this.txStateManager.setTxStatusFailed(txMeta.id, error) + throw error + } + txMeta.loadingDefaults = false + // save txMeta + this.txStateManager.updateTx(txMeta) + + return txMeta + } + + async addTxDefaults (txMeta) { + const txParams = txMeta.txParams + // ensure value + txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) + txMeta.nonceSpecified = Boolean(txParams.nonce) + let gasPrice = txParams.gasPrice + if (!gasPrice) { + gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice() + } + txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) + txParams.value = txParams.value || '0x0' + // set gasLimit + return await this.txGasUtil.analyzeGasUsage(txMeta) + } + + async retryTransaction (txId) { + this.txStateManager.setTxStatusUnapproved(txId) + const txMeta = this.txStateManager.getTx(txId) + txMeta.lastGasPrice = txMeta.txParams.gasPrice + this.txStateManager.updateTx(txMeta, 'retryTransaction: manual retry') + } + + async updateTransaction (txMeta) { + this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') + } + + async updateAndApproveTransaction (txMeta) { + this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') + await this.approveTransaction(txMeta.id) + } + + async approveTransaction (txId) { + let nonceLock + try { + // approve + this.txStateManager.setTxStatusApproved(txId) + // get next nonce + const txMeta = this.txStateManager.getTx(txId) + const fromAddress = txMeta.txParams.from + // wait for a nonce + nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + // add nonce to txParams + const nonce = txMeta.nonceSpecified ? txMeta.txParams.nonce : nonceLock.nextNonce + if (nonce > nonceLock.nextNonce) { + const message = `Specified nonce may not be larger than account's next valid nonce.` + throw new Error(message) + } + txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16)) + // add nonce debugging information to txMeta + txMeta.nonceDetails = nonceLock.nonceDetails + this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') + // sign transaction + const rawTx = await this.signTransaction(txId) + await this.publishTransaction(txId, rawTx) + // must set transaction to submitted/failed before releasing lock + nonceLock.releaseLock() + } catch (err) { + this.txStateManager.setTxStatusFailed(txId, err) + // must set transaction to submitted/failed before releasing lock + if (nonceLock) nonceLock.releaseLock() + // continue with error chain + throw err + } + } + + async signTransaction (txId) { + const txMeta = this.txStateManager.getTx(txId) + const txParams = txMeta.txParams + const fromAddress = txParams.from + // add network/chain id + txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16)) + const ethTx = new Transaction(txParams) + await this.signEthTx(ethTx, fromAddress) + this.txStateManager.setTxStatusSigned(txMeta.id) + const rawTx = ethUtil.bufferToHex(ethTx.serialize()) + return rawTx + } + + async publishTransaction (txId, rawTx) { + const txMeta = this.txStateManager.getTx(txId) + txMeta.rawTx = rawTx + this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction') + const txHash = await this.query.sendRawTransaction(rawTx) + this.setTxHash(txId, txHash) + this.txStateManager.setTxStatusSubmitted(txId) + } + + async cancelTransaction (txId) { + this.txStateManager.setTxStatusRejected(txId) + } + + // receives a txHash records the tx as signed + setTxHash (txId, txHash) { + // Add the tx hash to the persisted meta-tx object + const txMeta = this.txStateManager.getTx(txId) + txMeta.hash = txHash + this.txStateManager.updateTx(txMeta, 'transactions#setTxHash') + } + +// +// PRIVATE METHODS +// + + _updateMemstore () { + const unapprovedTxs = this.txStateManager.getUnapprovedTxList() + const selectedAddressTxList = this.txStateManager.getFilteredTxList({ + from: this.getSelectedAddress(), + metamaskNetworkId: this.getNetwork(), + }) + this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) + } +} diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 3196981ba..5e8577100 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -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', }, }, -} \ No newline at end of file +} diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 419f78cd6..9261e7d64 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -1,6 +1,7 @@ /*global Web3*/ cleanContextForImports() require('web3/dist/web3.min.js') +const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') // const PingStream = require('ping-pong-stream/ping') // const endOfStream = require('end-of-stream') @@ -8,6 +9,10 @@ const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js') restoreContextAfterImports() +const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' +window.log = log +log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') + // // setup plugin communication @@ -26,31 +31,23 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream) // setup web3 // +if (typeof window.web3 !== 'undefined') { + throw new Error(`MetaMask detected another web3. + MetaMask will not work reliably with another web3 extension. + This usually happens if you have two MetaMasks installed, + or MetaMask and another web3 extension. Please remove one + and try again.`) +} var web3 = new Web3(inpageProvider) web3.setProvider = function () { - console.log('MetaMask - overrode web3.setProvider') + log.debug('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) +log.debug('MetaMask - injected web3') +// export global web3, with usage-detection +setupDappAutoReload(web3, inpageProvider.publicConfigStore) // set web3 defaultAccount + inpageProvider.publicConfigStore.subscribe(function (state) { web3.eth.defaultAccount = state.selectedAddress }) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js deleted file mode 100644 index 16df6efa6..000000000 --- a/app/scripts/keyring-controller.js +++ /dev/null @@ -1,594 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const bip39 = require('bip39') -const EventEmitter = require('events').EventEmitter -const ObservableStore = require('obs-store') -const filter = require('promise-filter') -const encryptor = require('browser-passworder') -const sigUtil = require('eth-sig-util') -const normalizeAddress = sigUtil.normalize -// Keyrings: -const SimpleKeyring = require('eth-simple-keyring') -const HdKeyring = require('eth-hd-keyring') -const keyringTypes = [ - SimpleKeyring, - HdKeyring, -] - -class KeyringController extends EventEmitter { - - // PUBLIC METHODS - // - // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING, - // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS. - // - // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE. - - constructor (opts) { - super() - const initState = opts.initState || {} - this.keyringTypes = keyringTypes - this.store = new ObservableStore(initState) - this.memStore = new ObservableStore({ - isUnlocked: false, - keyringTypes: this.keyringTypes.map(krt => krt.type), - keyrings: [], - identities: {}, - }) - this.ethStore = opts.ethStore - this.encryptor = encryptor - this.keyrings = [] - this.getNetwork = opts.getNetwork - } - - // Full Update - // returns Promise( @object state ) - // - // Emits the `update` event and - // returns a Promise that resolves to the current state. - // - // Frequently used to end asynchronous chains in this class, - // indicating consumers can often either listen for updates, - // or accept a state-resolving promise to consume their results. - // - // Not all methods end with this, that might be a nice refactor. - fullUpdate () { - this.emit('update') - return Promise.resolve(this.memStore.getState()) - } - - // Create New Vault And Keychain - // @string password - The password to encrypt the vault with - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // randomly creates a new HD wallet with 1 account, - // faucets that account on the testnet. - createNewVaultAndKeychain (password) { - return this.persistAllKeyrings(password) - .then(this.createFirstKeyTree.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // CreateNewVaultAndRestore - // @string password - The password to encrypt the vault with - // @string seed - The BIP44-compliant seed phrase. - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // creates a new HD wallet from the given seed with 1 account. - createNewVaultAndRestore (password, seed) { - if (typeof password !== 'string') { - return Promise.reject('Password must be text.') - } - - if (!bip39.validateMnemonic(seed)) { - return Promise.reject('Seed phrase is invalid.') - } - - this.clearKeyrings() - - return this.persistAllKeyrings(password) - .then(() => { - return this.addNewKeyring('HD Key Tree', { - mnemonic: seed, - numberOfAccounts: 1, - }) - }) - .then((firstKeyring) => { - return firstKeyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - First Account not found.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this, password)) - .then(this.fullUpdate.bind(this)) - } - - // Set Locked - // returns Promise( @object state ) - // - // This method deallocates all secrets, and effectively locks metamask. - setLocked () { - // set locked - this.password = null - this.memStore.updateState({ isUnlocked: false }) - // remove keyrings - this.keyrings = [] - this._updateMemStoreKeyrings() - return this.fullUpdate() - } - - // Submit Password - // @string password - // - // returns Promise( @object state ) - // - // Attempts to decrypt the current vault and load its keyrings - // into memory. - // - // Temporarily also migrates any old-style vaults first, as well. - // (Pre MetaMask 3.0.0) - submitPassword (password) { - return this.unlockKeyrings(password) - .then((keyrings) => { - this.keyrings = keyrings - return this.fullUpdate() - }) - } - - // Add New Keyring - // @string type - // @object opts - // - // returns Promise( @Keyring keyring ) - // - // Adds a new Keyring of the given `type` to the vault - // and the current decrypted Keyrings array. - // - // All Keyring classes implement a unique `type` string, - // and this is used to retrieve them from the keyringTypes array. - addNewKeyring (type, opts) { - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring(opts) - return keyring.deserialize(opts) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.checkForDuplicate(type, accounts) - }) - .then((checkedAccounts) => { - this.keyrings.push(keyring) - return this.setupAccounts(checkedAccounts) - }) - .then(() => this.persistAllKeyrings()) - .then(() => this.fullUpdate()) - .then(() => { - this._updateMemStoreKeyrings() - return keyring - }) - } - - // For now just checks for simple key pairs - // but in the future - // should possibly add HD and other types - // - checkForDuplicate (type, newAccount) { - return this.getAccounts() - .then((accounts) => { - switch (type) { - case 'Simple Key Pair': - let 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) - } - }) - } - - - // Add New Account - // @number keyRingNum - // - // returns Promise( @object state ) - // - // Calls the `addAccounts` method on the Keyring - // in the kryings array at index `keyringNum`, - // and then saves those changes. - addNewAccount (selectedKeyring) { - return selectedKeyring.addAccounts(1) - .then(this.setupAccounts.bind(this)) - .then(this.persistAllKeyrings.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // Save Account Label - // @string account - // @string label - // - // returns Promise( @string label ) - // - // Persists a nickname equal to `label` for the specified account. - saveAccountLabel (account, label) { - try { - const hexAddress = normalizeAddress(account) - // update state on diskStore - const state = this.store.getState() - const walletNicknames = state.walletNicknames || {} - walletNicknames[hexAddress] = label - this.store.updateState({ walletNicknames }) - // update state on memStore - const identities = this.memStore.getState().identities - identities[hexAddress].name = label - this.memStore.updateState({ identities }) - return Promise.resolve(label) - } catch (err) { - return Promise.reject(err) - } - } - - // Export Account - // @string address - // - // returns Promise( @string privateKey ) - // - // Requests the private key from the keyring controlling - // the specified address. - // - // Returns a Promise that may resolve with the private key string. - exportAccount (address) { - try { - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.exportAccount(normalizeAddress(address)) - }) - } catch (e) { - return Promise.reject(e) - } - } - - - // SIGNING METHODS - // - // This method signs tx and returns a promise for - // TX Manager to update the state after signing - - signTransaction (ethTx, _fromAddress) { - const fromAddress = normalizeAddress(_fromAddress) - return this.getKeyringForAccount(fromAddress) - .then((keyring) => { - return keyring.signTransaction(fromAddress, ethTx) - }) - } - - // Sign Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - signMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signMessage(address, msgParams.data) - }) - } - - // Sign Personal Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - // Prefixes the hash before signing as per the new geth behavior. - signPersonalMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signPersonalMessage(address, msgParams.data) - }) - } - - // PRIVATE METHODS - // - // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER - // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS. - - // Create First Key Tree - // returns @Promise - // - // Clears the vault, - // creates a new one, - // creates a random new HD Keyring with 1 account, - // makes that account the selected account, - // faucets that account on testnet, - // puts the current seed words into the state tree. - createFirstKeyTree () { - this.clearKeyrings() - return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 }) - .then((keyring) => { - return keyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) - this.emit('newVault', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this)) - } - - // Setup Accounts - // @array accounts - // - // returns @Promise(@object account) - // - // Initializes the provided account array - // Gives them numerically incremented nicknames, - // and adds them to the ethStore for regular balance checking. - setupAccounts (accounts) { - return this.getAccounts() - .then((loadedAccounts) => { - const arr = accounts || loadedAccounts - return Promise.all(arr.map((account) => { - return this.getBalanceAndNickname(account) - })) - }) - } - - // Get Balance And Nickname - // @string account - // - // returns Promise( @string label ) - // - // Takes an account address and an iterator representing - // the current number of named accounts. - getBalanceAndNickname (account) { - if (!account) { - throw new Error('Problem loading account.') - } - const address = normalizeAddress(account) - this.ethStore.addAccount(address) - return this.createNickname(address) - } - - // Create Nickname - // @string address - // - // returns Promise( @string label ) - // - // Takes an address, and assigns it an incremented nickname, persisting it. - createNickname (address) { - const hexAddress = normalizeAddress(address) - const identities = this.memStore.getState().identities - const currentIdentityCount = Object.keys(identities).length + 1 - const nicknames = this.store.getState().walletNicknames || {} - const existingNickname = nicknames[hexAddress] - const name = existingNickname || `Account ${currentIdentityCount}` - identities[hexAddress] = { - address: hexAddress, - name, - } - this.memStore.updateState({ identities }) - return this.saveAccountLabel(hexAddress, name) - } - - // Persist All Keyrings - // @password string - // - // returns Promise - // - // Iterates the current `keyrings` array, - // serializes each one into a serialized array, - // encrypts that array with the provided `password`, - // and persists that encrypted string to storage. - persistAllKeyrings (password = this.password) { - if (typeof password === 'string') { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - } - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([keyring.type, keyring.serialize()]) - .then((serializedKeyringArray) => { - // Label the output values on each serialized Keyring: - return { - type: serializedKeyringArray[0], - data: serializedKeyringArray[1], - } - }) - })) - .then((serializedKeyrings) => { - return this.encryptor.encrypt(this.password, serializedKeyrings) - }) - .then((encryptedString) => { - this.store.updateState({ vault: encryptedString }) - return true - }) - } - - // Unlock Keyrings - // @string password - // - // returns Promise( @array keyrings ) - // - // Attempts to unlock the persisted encrypted storage, - // initializing the persisted keyrings to RAM. - unlockKeyrings (password) { - const encryptedVault = this.store.getState().vault - if (!encryptedVault) { - throw new Error('Cannot unlock without a previous vault.') - } - - return this.encryptor.decrypt(password, encryptedVault) - .then((vault) => { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - vault.forEach(this.restoreKeyring.bind(this)) - return this.keyrings - }) - } - - // Restore Keyring - // @object serialized - // - // returns Promise( @Keyring deserialized ) - // - // Attempts to initialize a new keyring from the provided - // serialized payload. - // - // On success, returns the resulting @Keyring instance. - restoreKeyring (serialized) { - const { type, data } = serialized - - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring() - return keyring.deserialize(data) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.setupAccounts(accounts) - }) - .then(() => { - this.keyrings.push(keyring) - this._updateMemStoreKeyrings() - return keyring - }) - } - - // Get Keyring Class For Type - // @string type - // - // Returns @class Keyring - // - // Searches the current `keyringTypes` array - // for a Keyring class whose unique `type` property - // matches the provided `type`, - // returning it if it exists. - getKeyringClassForType (type) { - return this.keyringTypes.find(kr => kr.type === type) - } - - getKeyringsByType (type) { - return this.keyrings.filter((keyring) => keyring.type === type) - } - - // Get Accounts - // returns Promise( @Array[ @string accounts ] ) - // - // Returns the public addresses of all current accounts - // managed by all currently unlocked keyrings. - getAccounts () { - const keyrings = this.keyrings || [] - return Promise.all(keyrings.map(kr => kr.getAccounts())) - .then((keyringArrays) => { - return keyringArrays.reduce((res, arr) => { - return res.concat(arr) - }, []) - }) - } - - // Get Keyring For Account - // @string address - // - // returns Promise(@Keyring keyring) - // - // Returns the currently initialized keyring that manages - // the specified `address` if one exists. - getKeyringForAccount (address) { - const hexed = normalizeAddress(address) - log.debug(`KeyringController - getKeyringForAccount: ${hexed}`) - - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([ - keyring, - keyring.getAccounts(), - ]) - })) - .then(filter((candidate) => { - const accounts = candidate[1].map(normalizeAddress) - return accounts.includes(hexed) - })) - .then((winners) => { - if (winners && winners.length > 0) { - return winners[0][0] - } else { - throw new Error('No keyring found for the requested account.') - } - }) - } - - // Display For Keyring - // @Keyring keyring - // - // returns Promise( @Object { type:String, accounts:Array } ) - // - // Is used for adding the current keyrings to the state object. - displayForKeyring (keyring) { - return keyring.getAccounts() - .then((accounts) => { - return { - type: keyring.type, - accounts: accounts, - } - }) - } - - // Add Gas Buffer - // @string gas (as hexadecimal value) - // - // returns @string bufferedGas (as hexadecimal value) - // - // Adds a healthy buffer of gas to an initial gas estimate. - addGasBuffer (gas) { - const gasBuffer = new BN('100000', 10) - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const correct = bnGas.add(gasBuffer) - return ethUtil.addHexPrefix(correct.toString(16)) - } - - // Clear Keyrings - // - // Deallocates all currently managed keyrings and accounts. - // Used before initializing a new vault. - clearKeyrings () { - let accounts - try { - accounts = Object.keys(this.ethStore.getState()) - } catch (e) { - accounts = [] - } - accounts.forEach((address) => { - this.ethStore.removeAccount(address) - }) - - // clear keyrings from memory - this.keyrings = [] - this.memStore.updateState({ - keyrings: [], - identities: {}, - }) - } - - _updateMemStoreKeyrings() { - Promise.all(this.keyrings.map(this.displayForKeyring)) - .then((keyrings) => { - this.memStore.updateState({ keyrings }) - }) - } - -} - -module.exports = KeyringController diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js new file mode 100644 index 000000000..8c3dd8c71 --- /dev/null +++ b/app/scripts/lib/account-tracker.js @@ -0,0 +1,125 @@ +/* Account Tracker + * + * This module is responsible for tracking any number of accounts + * and caching their current balances & transaction counts. + * + * It also tracks transaction hashes, and checks their inclusion status + * on each new block. + */ + +const async = require('async') +const EthQuery = require('eth-query') +const ObservableStore = require('obs-store') +const EventEmitter = require('events').EventEmitter +function noop () {} + + +class AccountTracker extends EventEmitter { + + constructor (opts = {}) { + super() + + const initState = { + accounts: {}, + currentBlockGasLimit: '', + } + this.store = new ObservableStore(initState) + + this._provider = opts.provider + this._query = new EthQuery(this._provider) + this._blockTracker = opts.blockTracker + // subscribe to latest block + this._blockTracker.on('block', this._updateForBlock.bind(this)) + // blockTracker.currentBlock may be null + this._currentBlockNumber = this._blockTracker.currentBlock + } + + // + // public + // + + syncWithAddresses (addresses) { + const accounts = this.store.getState().accounts + const locals = Object.keys(accounts) + + const toAdd = [] + addresses.forEach((upstream) => { + if (!locals.includes(upstream)) { + toAdd.push(upstream) + } + }) + + const toRemove = [] + locals.forEach((local) => { + if (!addresses.includes(local)) { + toRemove.push(local) + } + }) + + toAdd.forEach(upstream => this.addAccount(upstream)) + toRemove.forEach(local => this.removeAccount(local)) + this._updateAccounts() + } + + addAccount (address) { + const accounts = this.store.getState().accounts + accounts[address] = {} + this.store.updateState({ accounts }) + if (!this._currentBlockNumber) return + this._updateAccount(address) + } + + removeAccount (address) { + const accounts = this.store.getState().accounts + delete accounts[address] + this.store.updateState({ accounts }) + } + + // + // private + // + + _updateForBlock (block) { + this._currentBlockNumber = block.number + const currentBlockGasLimit = block.gasLimit + + this.store.updateState({ currentBlockGasLimit }) + + async.parallel([ + this._updateAccounts.bind(this), + ], (err) => { + if (err) return console.error(err) + this.emit('block', this.store.getState()) + }) + } + + _updateAccounts (cb = noop) { + const accounts = this.store.getState().accounts + const addresses = Object.keys(accounts) + async.each(addresses, this._updateAccount.bind(this), cb) + } + + _updateAccount (address, cb = noop) { + this._getAccount(address, (err, result) => { + if (err) return cb(err) + result.address = address + const accounts = this.store.getState().accounts + // only populate if the entry is still present + if (accounts[address]) { + accounts[address] = result + this.store.updateState({ accounts }) + } + cb(null, result) + }) + } + + _getAccount (address, cb = noop) { + const query = this._query + async.parallel({ + balance: query.getBalance.bind(query, address), + }, cb) + } + +} + +module.exports = AccountTracker diff --git a/app/scripts/lib/auto-faucet.js b/app/scripts/lib/auto-faucet.js deleted file mode 100644 index 38d54ba5e..000000000 --- a/app/scripts/lib/auto-faucet.js +++ /dev/null @@ -1,20 +0,0 @@ -const uri = 'https://faucet.metamask.io/' -const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' -const env = process.env.METAMASK_ENV - -module.exports = function (address) { - // Don't faucet in development or test - if (METAMASK_DEBUG === true || env === 'test') return - global.log.info('auto-fauceting:', address) - const data = address - const headers = new Headers() - headers.append('Content-type', 'application/rawdata') - fetch(uri, { - method: 'POST', - headers, - body: data, - }) - .catch((err) => { - console.error(err) - }) -} diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 1302df35f..cce31c3d2 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -1,30 +1,58 @@ -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 - })) + let hasBeenWarned = false + let reloadInProgress = false + let lastTimeUsed + let lastSeenNetwork - return handleResetRequest + global.web3 = new Proxy(web3, { + get: (_web3, key) => { + // show warning once on web3 access + if (!hasBeenWarned && key !== 'currentProvider') { + console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') + hasBeenWarned = true + } + // get the time of use + lastTimeUsed = Date.now() + // return value normally + return _web3[key] + }, + set: (_web3, key, value) => { + // set value normally + _web3[key] = value + }, + }) - function handleResetRequest () { - resetWasRequested = true - // ignore if web3 was not used - if (!pageIsUsingWeb3) return - // reload after short timeout - setTimeout(triggerReset, 500) - } + observable.subscribe(function (state) { + // if reload in progress, no need to check reload logic + if (reloadInProgress) return + + const currentNetwork = state.networkVersion + + // set the initial network + if (!lastSeenNetwork) { + lastSeenNetwork = currentNetwork + return + } + + // skip reload logic if web3 not used + if (!lastTimeUsed) return + + // if network did not change, exit + if (currentNetwork === lastSeenNetwork) return + + // initiate page reload + reloadInProgress = true + const timeSinceUse = Date.now() - lastTimeUsed + // if web3 was recently used then delay the reloading of the page + if (timeSinceUse > 500) { + triggerReset() + } else { + setTimeout(triggerReset, 500) + } + }) } // reload the page diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 91a1ec322..b9dde3c28 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -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 -} \ No newline at end of file +} diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index e31cb45ed..34b603b96 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -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) } @@ -71,6 +42,17 @@ ConfigManager.prototype.getData = function () { return this.store.getState() } +ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) { + const data = this.getData() + data.forgottenPassword = passwordForgottenState + this.setData(data) +} + +ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) { + const data = this.getData() + return data.forgottenPassword +} + ConfigManager.prototype.setWallet = function (wallet) { var data = this.getData() data.wallet = wallet @@ -136,6 +118,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 +156,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 } } diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js new file mode 100644 index 000000000..2707cbd9e --- /dev/null +++ b/app/scripts/lib/createLoggerMiddleware.js @@ -0,0 +1,15 @@ +// log rpc activity +module.exports = createLoggerMiddleware + +function createLoggerMiddleware ({ origin }) { + return function loggerMiddleware (req, res, next, end) { + next((cb) => { + if (res.error) { + log.error('Error in RPC response:\n', res) + } + if (req.isMetamaskInternal) return + log.info(`RPC (${origin}):`, req, '->', res) + cb() + }) + } +} diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js new file mode 100644 index 000000000..f8bdb2dc2 --- /dev/null +++ b/app/scripts/lib/createOriginMiddleware.js @@ -0,0 +1,9 @@ +// append dapp origin domain to request +module.exports = createOriginMiddleware + +function createOriginMiddleware ({ origin }) { + return function originMiddleware (req, res, next, end) { + req.origin = origin + next() + } +} diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js new file mode 100644 index 000000000..4e667bac2 --- /dev/null +++ b/app/scripts/lib/createProviderMiddleware.js @@ -0,0 +1,12 @@ +module.exports = createProviderMiddleware + +// forward requests to provider +function createProviderMiddleware ({ provider }) { + return (req, res, next, end) => { + provider.sendAsync(req, (err, _res) => { + if (err) return end(err) + res.result = _res.result + end() + }) + } +} diff --git a/app/scripts/lib/environment-type.js b/app/scripts/lib/environment-type.js new file mode 100644 index 000000000..7966926eb --- /dev/null +++ b/app/scripts/lib/environment-type.js @@ -0,0 +1,10 @@ +module.exports = function environmentType () { + const url = window.location.href + if (url.match(/popup.html$/)) { + return 'popup' + } else if (url.match(/home.html$/)) { + return 'responsive' + } else { + return 'notification' + } +} diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js deleted file mode 100644 index 243253df2..000000000 --- a/app/scripts/lib/eth-store.js +++ /dev/null @@ -1,136 +0,0 @@ -/* Ethereum Store - * - * This module is responsible for tracking any number of accounts - * and caching their current balances & transaction counts. - * - * It also tracks transaction hashes, and checks their inclusion status - * on each new block. - */ - -const async = require('async') -const EthQuery = require('eth-query') -const ObservableStore = require('obs-store') -function noop() {} - - -class EthereumStore extends ObservableStore { - - constructor (opts = {}) { - super({ - accounts: {}, - transactions: {}, - currentBlockNumber: '0', - currentBlockHash: '', - }) - this._provider = opts.provider - this._query = new EthQuery(this._provider) - this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('block', this._updateForBlock.bind(this)) - // blockTracker.currentBlock may be null - this._currentBlockNumber = this._blockTracker.currentBlock - } - - // - // public - // - - addAccount (address) { - const accounts = this.getState().accounts - accounts[address] = {} - this.updateState({ accounts }) - if (!this._currentBlockNumber) return - this._updateAccount(address) - } - - removeAccount (address) { - const accounts = this.getState().accounts - delete accounts[address] - this.updateState({ accounts }) - } - - addTransaction (txHash) { - const transactions = this.getState().transactions - transactions[txHash] = {} - this.updateState({ transactions }) - if (!this._currentBlockNumber) return - this._updateTransaction(this._currentBlockNumber, txHash, noop) - } - - removeTransaction (txHash) { - const transactions = this.getState().transactions - delete transactions[txHash] - this.updateState({ transactions }) - } - - - // - // private - // - - _updateForBlock (block) { - const blockNumber = '0x' + block.number.toString('hex') - this._currentBlockNumber = blockNumber - this.updateState({ currentBlockNumber: parseInt(blockNumber) }) - this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) - async.parallel([ - this._updateAccounts.bind(this), - this._updateTransactions.bind(this, blockNumber), - ], (err) => { - if (err) return console.error(err) - this.emit('block', this.getState()) - }) - } - - _updateAccounts (cb = noop) { - const accounts = this.getState().accounts - const addresses = Object.keys(accounts) - async.each(addresses, this._updateAccount.bind(this), cb) - } - - _updateAccount (address, cb = noop) { - const accounts = this.getState().accounts - this._getAccount(address, (err, result) => { - if (err) return cb(err) - result.address = address - // only populate if the entry is still present - if (accounts[address]) { - accounts[address] = result - this.updateState({ accounts }) - } - cb(null, result) - }) - } - - _updateTransactions (block, cb = noop) { - const transactions = this.getState().transactions - const txHashes = Object.keys(transactions) - async.each(txHashes, this._updateTransaction.bind(this, block), cb) - } - - _updateTransaction (block, txHash, cb = noop) { - // would use the block here to determine how many confirmations the tx has - const transactions = this.getState().transactions - this._query.getTransaction(txHash, (err, result) => { - if (err) return cb(err) - // only populate if the entry is still present - if (transactions[txHash]) { - transactions[txHash] = result - this.updateState({ transactions }) - } - cb(null, result) - }) - } - - _getAccount (address, cb = noop) { - const query = this._query - async.parallel({ - balance: query.getBalance.bind(query, address), - nonce: query.getTransactionCount.bind(query, address), - code: query.getCode.bind(query, address), - }, cb) - } - -} - -module.exports = EthereumStore diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js new file mode 100644 index 000000000..c0a490b05 --- /dev/null +++ b/app/scripts/lib/events-proxy.js @@ -0,0 +1,31 @@ +module.exports = function createEventEmitterProxy (eventEmitter, listeners) { + let target = eventEmitter + const eventHandlers = listeners || {} + const proxy = new Proxy({}, { + get: (obj, name) => { + // intercept listeners + if (name === 'on') return addListener + if (name === 'setTarget') return setTarget + if (name === 'proxyEventHandlers') return eventHandlers + return target[name] + }, + set: (obj, name, value) => { + target[name] = value + return true + }, + }) + function setTarget (eventEmitter) { + target = eventEmitter + // migrate listeners + Object.keys(eventHandlers).forEach((name) => { + eventHandlers[name].forEach((handler) => target.on(name, handler)) + }) + } + function addListener (name, handler) { + if (!eventHandlers[name]) eventHandlers[name] = [] + eventHandlers[name].push(handler) + target.on(name, handler) + } + if (listeners) proxy.setTarget(eventEmitter) + return proxy +} diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 92936de2f..99cc5d2cf 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,8 +1,10 @@ -const pipe = require('pump') -const StreamProvider = require('web3-stream-provider') +const pump = require('pump') +const RpcEngine = require('json-rpc-engine') +const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') +const createStreamMiddleware = require('json-rpc-middleware-stream') const LocalStorageStore = require('obs-store') -const ObjectMultiplex = require('./obj-multiplex') -const createRandomId = require('./random-id') +const asStream = require('obs-store/lib/asStream') +const ObjectMultiplex = require('obj-multiplex') module.exports = MetamaskInpageProvider @@ -10,56 +12,50 @@ function MetamaskInpageProvider (connectionStream) { const self = this // setup connectionStream multiplexing - var multiStream = self.multiStream = ObjectMultiplex() - pipe( + const mux = self.mux = new ObjectMultiplex() + pump( connectionStream, - multiStream, + mux, connectionStream, (err) => logStreamDisconnectWarning('MetaMask', err) ) // subscribe to metamask public config (one-way) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) - pipe( - multiStream.createStream('publicConfig'), - self.publicConfigStore, + + pump( + mux.createStream('publicConfig'), + asStream(self.publicConfigStore), (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) ) + // ignore phishing warning message (handled elsewhere) + mux.ignoreStream('phishing') + // connect to async provider - const asyncProvider = self.asyncProvider = new StreamProvider() - pipe( - asyncProvider, - multiStream.createStream('provider'), - asyncProvider, + const streamMiddleware = createStreamMiddleware() + pump( + streamMiddleware.stream, + mux.createStream('provider'), + streamMiddleware.stream, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) - self.idMap = {} - // handle sendAsync requests via asyncProvider - self.sendAsync = function (payload, cb) { - // rewrite request ids - var request = eachJsonMessage(payload, (message) => { - var newId = createRandomId() - self.idMap[newId] = message.id - message.id = newId - return message - }) - // forward to asyncProvider - asyncProvider.sendAsync(request, function (err, res) { - if (err) return cb(err) - // transform messages to original ids - eachJsonMessage(res, (message) => { - var oldId = self.idMap[message.id] - delete self.idMap[message.id] - message.id = oldId - return message - }) - cb(null, res) - }) - } + // handle sendAsync requests via dapp-side rpc engine + const rpcEngine = new RpcEngine() + rpcEngine.push(createIdRemapMiddleware()) + rpcEngine.push(streamMiddleware) + self.rpcEngine = rpcEngine } +// handle sendAsync requests via asyncProvider +// also remap ids inbound and outbound +MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { + const self = this + self.rpcEngine.handle(payload, cb) +} + + MetamaskInpageProvider.prototype.send = function (payload) { const self = this @@ -76,7 +72,7 @@ MetamaskInpageProvider.prototype.send = function (payload) { case 'eth_coinbase': // read from localStorage selectedAddress = self.publicConfigStore.getState().selectedAddress - result = selectedAddress + result = selectedAddress || null break case 'eth_uninstallFilter': @@ -85,8 +81,8 @@ MetamaskInpageProvider.prototype.send = function (payload) { break case 'net_version': - let networkVersion = self.publicConfigStore.getState().networkVersion - result = networkVersion + const networkVersion = self.publicConfigStore.getState().networkVersion + result = networkVersion || null break // throw not-supported Error @@ -105,10 +101,6 @@ MetamaskInpageProvider.prototype.send = function (payload) { } } -MetamaskInpageProvider.prototype.sendAsync = function () { - throw new Error('MetamaskInpageProvider - sendAsync not overwritten') -} - MetamaskInpageProvider.prototype.isConnected = function () { return true } @@ -117,15 +109,7 @@ MetamaskInpageProvider.prototype.isMetaMask = true // util -function eachJsonMessage (payload, transformFn) { - if (Array.isArray(payload)) { - return payload.map(transformFn) - } else { - return transformFn(payload) - } -} - -function logStreamDisconnectWarning(remoteLabel, err){ +function logStreamDisconnectWarning (remoteLabel, err) { let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` if (err) warningMsg += '\n' + err.stack console.warn(warningMsg) diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js index 693fa8751..e2999411f 100644 --- a/app/scripts/lib/is-popup-or-notification.js +++ b/app/scripts/lib/is-popup-or-notification.js @@ -1,6 +1,9 @@ module.exports = function isPopupOrNotification () { const url = window.location.href - if (url.match(/popup.html$/)) { + // if (url.match(/popup.html$/) || url.match(/home.html$/)) { + // Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js) + // Revert below regexes to above commented out regexes before merge to master + if (url.match(/popup.html(?:\?.+)*$/) || url.match(/home.html(?:\?.+)*$/)) { return 'popup' } else { return 'notification' diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 711d5f159..f52e048e0 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -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 diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index 312345263..4fd2cae92 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -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: { diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 51d89a8fb..9b595d93c 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,24 +1,18 @@ -module.exports = function (promiseFn) { +const promiseToCallback = require('promise-to-callback') +const noop = function () {} + +module.exports = function nodeify (fn, context) { return function () { - var args = [] - for (var i = 0; i < arguments.length - 1; i++) { - args.push(arguments[i]) + const args = [].slice.call(arguments) + const lastArg = args[args.length - 1] + const lastArgIsCallback = typeof lastArg === 'function' + let callback + if (lastArgIsCallback) { + callback = lastArg + args.pop() + } else { + callback = noop } - var cb = arguments[arguments.length - 1] - - const nodeified = promiseFn.apply(this, args) - - if (!nodeified) { - const methodName = String(promiseFn).split('(')[0] - throw new Error(`The ${methodName} did not return a Promise, but was nodeified.`) - } - nodeified.then(function (result) { - cb(null, result) - }) - .catch(function (reason) { - cb(reason) - }) - - return nodeified + promiseToCallback(fn.apply(context, args))(callback) } } diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js new file mode 100644 index 000000000..ed9dd3f11 --- /dev/null +++ b/app/scripts/lib/nonce-tracker.js @@ -0,0 +1,149 @@ +const EthQuery = require('ethjs-query') +const assert = require('assert') +const Mutex = require('await-semaphore').Mutex + +class NonceTracker { + + constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { + this.provider = provider + this.ethQuery = new EthQuery(provider) + this.getPendingTransactions = getPendingTransactions + this.getConfirmedTransactions = getConfirmedTransactions + this.lockMap = {} + } + + async getGlobalLock () { + const globalMutex = this._lookupMutex('global') + // await global mutex free + const releaseLock = await globalMutex.acquire() + return { releaseLock } + } + + // releaseLock must be called + // releaseLock must be called after adding signed tx to pending transactions (or discarding) + async getNonceLock (address) { + // await global mutex free + await this._globalMutexFree() + // await lock free, then take lock + const releaseLock = await this._takeMutex(address) + // evaluate multiple nextNonce strategies + const nonceDetails = {} + const networkNonceResult = await this._getNetworkNextNonce(address) + const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address) + const nextNetworkNonce = networkNonceResult.nonce + const highestLocalNonce = highestLocallyConfirmed + const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce) + + const pendingTxs = this.getPendingTransactions(address) + const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0 + + nonceDetails.params = { + highestLocalNonce, + highestSuggested, + nextNetworkNonce, + } + nonceDetails.local = localNonceResult + nonceDetails.network = networkNonceResult + + const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce) + assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) + + // return nonce and release cb + return { nextNonce, nonceDetails, releaseLock } + } + + async _getCurrentBlock () { + const blockTracker = this._getBlockTracker() + const currentBlock = blockTracker.getCurrentBlock() + if (currentBlock) return currentBlock + return await new Promise((reject, resolve) => { + blockTracker.once('latest', resolve) + }) + } + + async _globalMutexFree () { + const globalMutex = this._lookupMutex('global') + const release = await globalMutex.acquire() + release() + } + + async _takeMutex (lockId) { + const mutex = this._lookupMutex(lockId) + const releaseLock = await mutex.acquire() + return releaseLock + } + + _lookupMutex (lockId) { + let mutex = this.lockMap[lockId] + if (!mutex) { + mutex = new Mutex() + this.lockMap[lockId] = mutex + } + return mutex + } + + async _getNetworkNextNonce (address) { + // calculate next nonce + // we need to make sure our base count + // and pending count are from the same block + const currentBlock = await this._getCurrentBlock() + const blockNumber = currentBlock.blockNumber + const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest') + const baseCount = baseCountBN.toNumber() + assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) + const nonceDetails = { blockNumber, baseCount } + return { name: 'network', nonce: baseCount, details: nonceDetails } + } + + _getHighestLocallyConfirmed (address) { + const confirmedTransactions = this.getConfirmedTransactions(address) + const highest = this._getHighestNonce(confirmedTransactions) + return Number.isInteger(highest) ? highest + 1 : 0 + } + + _reduceTxListToUniqueNonces (txList) { + const reducedTxList = txList.reduce((reducedList, txMeta, index) => { + if (!index) return [txMeta] + const nonceMatches = txList.filter((txData) => { + return txMeta.txParams.nonce === txData.txParams.nonce + }) + if (nonceMatches.length > 1) return reducedList + reducedList.push(txMeta) + return reducedList + }, []) + return reducedTxList + } + + _getHighestNonce (txList) { + const nonces = txList.map((txMeta) => { + const nonce = txMeta.txParams.nonce + assert(typeof nonce, 'string', 'nonces should be hex strings') + return parseInt(nonce, 16) + }) + const highestNonce = Math.max.apply(null, nonces) + return highestNonce + } + + _getHighestContinuousFrom (txList, startPoint) { + const nonces = txList.map((txMeta) => { + const nonce = txMeta.txParams.nonce + assert(typeof nonce, 'string', 'nonces should be hex strings') + return parseInt(nonce, 16) + }) + + let highest = startPoint + while (nonces.includes(highest)) { + highest++ + } + + return { name: 'local', nonce: highest, details: { startPoint, highest } } + } + + // this is a hotfix for the fact that the blockTracker will + // change when the network changes + _getBlockTracker () { + return this.provider._blockTracker + } +} + +module.exports = NonceTracker diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 55e5b8dd2..adaf60c65 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -1,5 +1,5 @@ const extension = require('extensionizer') -const height = 520 +const height = 620 const width = 360 @@ -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 \ No newline at end of file +module.exports = NotificationManager diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js deleted file mode 100644 index bd114c394..000000000 --- a/app/scripts/lib/obj-multiplex.js +++ /dev/null @@ -1,44 +0,0 @@ -const through = require('through2') - -module.exports = ObjectMultiplex - -function ObjectMultiplex (opts) { - opts = opts || {} - // create multiplexer - var mx = through.obj(function (chunk, enc, cb) { - var name = chunk.name - var data = chunk.data - var substream = mx.streams[name] - if (!substream) { - console.warn(`orphaned data for stream "${name}"`) - } else { - if (substream.push) substream.push(data) - } - return cb() - }) - mx.streams = {} - // create substreams - mx.createStream = function (name) { - var substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { - mx.push({ - name: name, - data: chunk, - }) - return cb() - }) - mx.on('end', function () { - return substream.emit('end') - }) - if (opts.error) { - mx.on('error', function () { - return substream.emit('error') - }) - } - return substream - } - // ignore streams (dont display orphaned data warning) - mx.ignoreStream = function (name) { - mx.streams[name] = true - } - return mx -} diff --git a/app/scripts/lib/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js new file mode 100644 index 000000000..6ae526463 --- /dev/null +++ b/app/scripts/lib/pending-balance-calculator.js @@ -0,0 +1,51 @@ +const BN = require('ethereumjs-util').BN +const normalize = require('eth-sig-util').normalize + +class PendingBalanceCalculator { + + // Must be initialized with two functions: + // getBalance => Returns a promise of a BN of the current balance in Wei + // getPendingTransactions => Returns an array of TxMeta Objects, + // which have txParams properties, which include value, gasPrice, and gas, + // all in a base=16 hex format. + constructor ({ getBalance, getPendingTransactions }) { + this.getPendingTransactions = getPendingTransactions + this.getNetworkBalance = getBalance + } + + async getBalance () { + const results = await Promise.all([ + this.getNetworkBalance(), + this.getPendingTransactions(), + ]) + + const [ balance, pending ] = results + if (!balance) return undefined + + const pendingValue = pending.reduce((total, tx) => { + return total.add(this.calculateMaxCost(tx)) + }, new BN(0)) + + return `0x${balance.sub(pendingValue).toString(16)}` + } + + calculateMaxCost (tx) { + const txValue = tx.txParams.value + const value = this.hexToBn(txValue) + const gasPrice = this.hexToBn(tx.txParams.gasPrice) + + const gas = tx.txParams.gas + const gasLimit = tx.txParams.gasLimit + const gasLimitBn = this.hexToBn(gas || gasLimit) + + const gasCost = gasPrice.mul(gasLimitBn) + return value.add(gasCost) + } + + hexToBn (hex) { + return new BN(normalize(hex).substring(2), 16) + } + +} + +module.exports = PendingBalanceCalculator diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js new file mode 100644 index 000000000..e8869e6b8 --- /dev/null +++ b/app/scripts/lib/pending-tx-tracker.js @@ -0,0 +1,189 @@ +const EventEmitter = require('events') +const EthQuery = require('ethjs-query') +/* + + Utility class for tracking the transactions as they + go from a pending state to a confirmed (mined in a block) state + + As well as continues broadcast while in the pending state + + ~config is not optional~ + requires a: { + provider: //, + nonceTracker: //see nonce tracker, + getPendingTransactions: //() a function for getting an array of transactions, + publishTransaction: //(rawTx) a async function for publishing raw transactions, + } + +*/ + +module.exports = class PendingTransactionTracker extends EventEmitter { + constructor (config) { + super() + this.query = new EthQuery(config.provider) + this.nonceTracker = config.nonceTracker + // default is one day + this.getPendingTransactions = config.getPendingTransactions + this.getCompletedTransactions = config.getCompletedTransactions + this.publishTransaction = config.publishTransaction + this._checkPendingTxs() + } + + // checks if a signed tx is in a block and + // if included sets the tx status as 'confirmed' + checkForTxInBlock (block) { + const signedTxList = this.getPendingTransactions() + if (!signedTxList.length) return + signedTxList.forEach((txMeta) => { + const txHash = txMeta.hash + const txId = txMeta.id + + if (!txHash) { + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.emit('tx:failed', txId, noTxHashErr) + return + } + + + block.transactions.forEach((tx) => { + if (tx.hash === txHash) this.emit('tx:confirmed', txId) + }) + }) + } + + queryPendingTxs ({ oldBlock, newBlock }) { + // check pending transactions on start + if (!oldBlock) { + this._checkPendingTxs() + return + } + // if we synced by more than one block, check for missed pending transactions + const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16) + if (diff > 1) this._checkPendingTxs() + } + + + resubmitPendingTxs (block) { + const pending = this.getPendingTransactions() + // only try resubmitting if their are transactions to resubmit + if (!pending.length) return + pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => { + /* + Dont marked as failed if the error is a "known" transaction warning + "there is already a transaction with the same sender-nonce + but higher/same gas price" + + Also don't mark as failed if it has ever been broadcast successfully. + A successful broadcast means it may still be mined. + */ + const errorMessage = err.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage.includes('replacement transaction underpriced') || + errorMessage.includes('known transaction') || + // parity + errorMessage.includes('gas price too low to replace') || + errorMessage.includes('transaction with the same hash was already imported') || + // other + errorMessage.includes('gateway timeout') || + errorMessage.includes('nonce too low') + ) + // ignore resubmit warnings, return early + if (isKnownTx) return + // encountered real error - transition to error state + txMeta.warning = { + error: errorMessage, + message: 'There was an error when resubmitting this transaction.', + } + this.emit('tx:warning', txMeta, err) + })) + } + + async _resubmitTx (txMeta, latestBlockNumber) { + if (!txMeta.firstRetryBlockNumber) { + this.emit('tx:block-update', txMeta, latestBlockNumber) + } + + const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber + const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16) + + const retryCount = txMeta.retryCount || 0 + + // Exponential backoff to limit retries at publishing + if (txBlockDistance <= Math.pow(2, retryCount) - 1) return + + // Only auto-submit already-signed txs: + if (!('rawTx' in txMeta)) return + + const rawTx = txMeta.rawTx + const txHash = await this.publishTransaction(rawTx) + + // Increment successful tries: + this.emit('tx:retry', txMeta) + return txHash + } + + async _checkPendingTx (txMeta) { + const txHash = txMeta.hash + const txId = txMeta.id + + // extra check in case there was an uncaught error during the + // signature and submission process + if (!txHash) { + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.emit('tx:failed', txId, noTxHashErr) + return + } + + // If another tx with the same nonce is mined, set as failed. + const taken = await this._checkIfNonceIsTaken(txMeta) + if (taken) { + const nonceTakenErr = new Error('Another transaction with this nonce has been mined.') + nonceTakenErr.name = 'NonceTakenErr' + return this.emit('tx:failed', txId, nonceTakenErr) + } + + // get latest transaction status + let txParams + try { + txParams = await this.query.getTransactionByHash(txHash) + if (!txParams) return + if (txParams.blockNumber) { + this.emit('tx:confirmed', txId) + } + } catch (err) { + txMeta.warning = { + error: err.message, + message: 'There was a problem loading this transaction.', + } + this.emit('tx:warning', txMeta, err) + } + } + + // checks the network for signed txs and + // if confirmed sets the tx status as 'confirmed' + async _checkPendingTxs () { + const signedTxList = this.getPendingTransactions() + // in order to keep the nonceTracker accurate we block it while updating pending transactions + const nonceGlobalLock = await this.nonceTracker.getGlobalLock() + try { + await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) + } catch (err) { + console.error('PendingTransactionWatcher - Error updating pending transactions') + console.error(err) + } + nonceGlobalLock.releaseLock() + } + + async _checkIfNonceIsTaken (txMeta) { + const address = txMeta.txParams.from + const completed = this.getCompletedTransactions(address) + const sameNonce = completed.filter((otherMeta) => { + return otherMeta.txParams.nonce === txMeta.txParams.nonce + }) + return sameNonce.length > 0 + } + +} diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index bbc978446..6602f5aa8 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -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)) { diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js index 607a9c9ed..a9716fb00 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -1,5 +1,6 @@ const Duplex = require('readable-stream').Duplex const inherits = require('util').inherits +const noop = function () {} module.exports = PortDuplexStream @@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) { if (Buffer.isBuffer(msg)) { delete msg._isBuffer var data = new Buffer(msg) - // console.log('PortDuplexStream - saw message as buffer', data) this.push(data) } else { - // console.log('PortDuplexStream - saw message', msg) this.push(msg) } } PortDuplexStream.prototype._onDisconnect = function () { - try { - this.push(null) - } catch (err) { - this.emit('error', err) - } + this.destroy() } // stream plumbing @@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) { if (Buffer.isBuffer(msg)) { var data = msg.toJSON() data._isBuffer = true - // console.log('PortDuplexStream - sent message as buffer', data) this._port.postMessage(data) } else { - // console.log('PortDuplexStream - sent message', msg) this._port.postMessage(msg) } } catch (err) { - // console.error(err) return cb(new Error('PortDuplexStream - disconnected')) } cb() } - -// util - -function noop () {} diff --git a/app/scripts/lib/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js new file mode 100644 index 000000000..40343f017 --- /dev/null +++ b/app/scripts/lib/setupMetamaskMeshMetrics.js @@ -0,0 +1,9 @@ + +module.exports = setupMetamaskMeshMetrics + +function setupMetamaskMeshMetrics() { + const testingContainer = document.createElement('iframe') + testingContainer.src = 'https://metamask.github.io/mesh-testing/' + console.log('Injecting MetaMask Mesh testing client') + document.head.appendChild(testingContainer) +} diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js index ba79990cc..8bb0b4f3c 100644 --- a/app/scripts/lib/stream-utils.js +++ b/app/scripts/lib/stream-utils.js @@ -1,6 +1,6 @@ const Through = require('through2') -const endOfStream = require('end-of-stream') -const ObjectMultiplex = require('./obj-multiplex') +const ObjectMultiplex = require('obj-multiplex') +const pump = require('pump') module.exports = { jsonParseStream: jsonParseStream, @@ -23,14 +23,14 @@ function jsonStringifyStream () { } function setupMultiplex (connectionStream) { - var mx = ObjectMultiplex() - connectionStream.pipe(mx).pipe(connectionStream) - endOfStream(mx, function (err) { - if (err) console.error(err) - }) - endOfStream(connectionStream, function (err) { - if (err) console.error(err) - mx.destroy() - }) - return mx + const mux = new ObjectMultiplex() + pump( + connectionStream, + mux, + connectionStream, + (err) => { + if (err) console.error(err) + } + ) + return mux } diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/lib/tx-gas-utils.js new file mode 100644 index 000000000..6f6ff7852 --- /dev/null +++ b/app/scripts/lib/tx-gas-utils.js @@ -0,0 +1,125 @@ +const EthQuery = require('ethjs-query') +const { + hexToBn, + BnMultiplyByFraction, + bnToHex, +} = require('./util') +const addHexPrefix = require('ethereumjs-util').addHexPrefix +const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. + +/* +tx-utils are utility methods for Transaction manager +its passed ethquery +and used to do things like calculate gas of a tx. +*/ + +module.exports = class TxGasUtil { + + constructor (provider) { + this.query = new EthQuery(provider) + } + + async analyzeGasUsage (txMeta) { + const block = await this.query.getBlockByNumber('latest', true) + let estimatedGasHex + try { + estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) + } catch (err) { + const simulationFailed = ( + err.message.includes('Transaction execution error.') || + err.message.includes('gas required exceeds allowance or always failing transaction') + ) + if (simulationFailed) { + txMeta.simulationFails = true + return txMeta + } + } + this.setTxGas(txMeta, block.gasLimit, estimatedGasHex) + return txMeta + } + + async estimateTxGas (txMeta, blockGasLimitHex) { + const txParams = txMeta.txParams + + // check if gasLimit is already specified + txMeta.gasLimitSpecified = Boolean(txParams.gas) + + // if it is, use that value + if (txMeta.gasLimitSpecified) { + return txParams.gas + } + + // if recipient has no code, gas is 21k max: + const recipient = txParams.to + const hasRecipient = Boolean(recipient) + const code = await this.query.getCode(recipient) + if (hasRecipient && (!code || code === '0x')) { + txParams.gas = SIMPLE_GAS_COST + txMeta.simpleSend = true // Prevents buffer addition + return SIMPLE_GAS_COST + } + + // if not, fall back to block gasLimit + const blockGasLimitBN = hexToBn(blockGasLimitHex) + const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20) + txParams.gas = bnToHex(saferGasLimitBN) + + // run tx + return await this.query.estimateGas(txParams) + } + + setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { + txMeta.estimatedGas = addHexPrefix(estimatedGasHex) + const txParams = txMeta.txParams + + // if gasLimit was specified and doesnt OOG, + // use original specified amount + if (txMeta.gasLimitSpecified || txMeta.simpleSend) { + txMeta.estimatedGas = txParams.gas + return + } + // if gasLimit not originally specified, + // try adding an additional gas buffer to our estimation for safety + const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex) + txParams.gas = recommendedGasHex + return + } + + addGasBuffer (initialGasLimitHex, blockGasLimitHex) { + const initialGasLimitBn = hexToBn(initialGasLimitHex) + const blockGasLimitBn = hexToBn(blockGasLimitHex) + const upperGasLimitBn = blockGasLimitBn.muln(0.9) + const bufferedGasLimitBn = initialGasLimitBn.muln(1.5) + + // if initialGasLimit is above blockGasLimit, dont modify it + if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn) + // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit + if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn) + // otherwise use blockGasLimit + return bnToHex(upperGasLimitBn) + } + + async validateTxParams (txParams) { + this.validateRecipient(txParams) + if ('value' in txParams) { + const value = txParams.value.toString() + if (value.includes('-')) { + throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) + } + + if (value.includes('.')) { + throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`) + } + } + } + validateRecipient (txParams) { + if (txParams.to === '0x') { + if (txParams.data) { + delete txParams.to + } else { + throw new Error('Invalid recipient address') + } + } + return txParams + } +} diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js new file mode 100644 index 000000000..94c7b6792 --- /dev/null +++ b/app/scripts/lib/tx-state-history-helper.js @@ -0,0 +1,41 @@ +const jsonDiffer = require('fast-json-patch') +const clone = require('clone') + +module.exports = { + generateHistoryEntry, + replayHistory, + snapshotFromTxMeta, + migrateFromSnapshotsToDiffs, +} + + +function migrateFromSnapshotsToDiffs (longHistory) { + return ( + longHistory + // convert non-initial history entries into diffs + .map((entry, index) => { + if (index === 0) return entry + return generateHistoryEntry(longHistory[index - 1], entry) + }) + ) +} + +function generateHistoryEntry (previousState, newState, note) { + const entry = jsonDiffer.compare(previousState, newState) + // Add a note to the first op, since it breaks if we append it to the entry + if (note && entry[0]) entry[0].note = note + return entry +} + +function replayHistory (_shortHistory) { + const shortHistory = clone(_shortHistory) + return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) +} + +function snapshotFromTxMeta (txMeta) { + // create txMeta snapshot for history + const snapshot = clone(txMeta) + // dont include previous history in this snapshot + delete snapshot.history + return snapshot +} diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js new file mode 100644 index 000000000..051efd247 --- /dev/null +++ b/app/scripts/lib/tx-state-manager.js @@ -0,0 +1,266 @@ +const extend = require('xtend') +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const txStateHistoryHelper = require('./tx-state-history-helper') + +module.exports = class TransactionStateManger extends EventEmitter { + constructor ({ initState, txHistoryLimit, getNetwork }) { + super() + + this.store = new ObservableStore( + extend({ + transactions: [], + }, initState)) + this.txHistoryLimit = txHistoryLimit + this.getNetwork = getNetwork + } + + // Returns the number of txs for the current network. + getTxCount () { + return this.getTxList().length + } + + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) + } + + getFullTxList () { + return this.store.getState().transactions + } + + // Returns the tx list + getUnapprovedTxList () { + const txList = this.getTxsByMetaData('status', 'unapproved') + return txList.reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + getPendingTransactions (address) { + const opts = { status: 'submitted' } + if (address) opts.from = address + return this.getFilteredTxList(opts) + } + + getConfirmedTransactions (address) { + const opts = { status: 'confirmed' } + if (address) opts.from = address + return this.getFilteredTxList(opts) + } + + addTx (txMeta) { + this.once(`${txMeta.id}:signed`, function (txId) { + this.removeAllListeners(`${txMeta.id}:rejected`) + }) + this.once(`${txMeta.id}:rejected`, function (txId) { + this.removeAllListeners(`${txMeta.id}:signed`) + }) + // initialize history + txMeta.history = [] + // capture initial snapshot of txMeta for history + const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta) + txMeta.history.push(snapshot) + + const transactions = this.getFullTxList() + const txCount = this.getTxCount() + const txHistoryLimit = this.txHistoryLimit + + // checks if the length of the tx history is + // longer then desired persistence limit + // and then if it is removes only confirmed + // or rejected tx's. + // not tx's that are pending or unapproved + if (txCount > txHistoryLimit - 1) { + const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') + transactions.splice(index, 1) + } + transactions.push(txMeta) + this._saveTxList(transactions) + return txMeta + } + // gets tx by Id and returns it + getTx (txId) { + const txMeta = this.getTxsByMetaData('id', txId)[0] + return txMeta + } + + updateTx (txMeta, note) { + if (txMeta.txParams) { + Object.keys(txMeta.txParams).forEach((key) => { + const value = txMeta.txParams[key] + if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`) + if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed') + }) + } + + // create txMeta snapshot for history + const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta) + // recover previous tx state obj + const previousState = txStateHistoryHelper.replayHistory(txMeta.history) + // generate history entry and add to history + const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note) + txMeta.history.push(entry) + + // commit txMeta to state + const txId = txMeta.id + const txList = this.getFullTxList() + const index = txList.findIndex(txData => txData.id === txId) + txList[index] = txMeta + this._saveTxList(txList) + } + + + // merges txParams obj onto txData.txParams + // use extend to ensure that all fields are filled + updateTxParams (txId, txParams) { + const txMeta = this.getTx(txId) + txMeta.txParams = extend(txMeta.txParams, txParams) + this.updateTx(txMeta, `txStateManager#updateTxParams`) + } + +/* + Takes an object of fields to search for eg: + let thingsToLookFor = { + to: '0x0..', + from: '0x0..', + status: 'signed', + err: undefined, + } + and returns a list of tx with all + options matching + + ****************HINT**************** + | `err: undefined` is like looking | + | for a tx with no err | + | so you can also search txs that | + | dont have something as well by | + | setting the value as undefined | + ************************************ + + this is for things like filtering a the tx list + for only tx's from 1 account + or for filltering for all txs from one account + and that have been 'confirmed' + */ + getFilteredTxList (opts, initialList) { + let filteredTxList = initialList + Object.keys(opts).forEach((key) => { + filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) + }) + return filteredTxList + } + + getTxsByMetaData (key, value, txList = this.getTxList()) { + return txList.filter((txMeta) => { + if (txMeta.txParams[key]) { + return txMeta.txParams[key] === value + } else { + return txMeta[key] === value + } + }) + } + + // STATUS METHODS + // statuses: + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'approved'` the user has approved the tx + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + // - `'failed'` the tx failed for some reason, included on tx data. + + // get::set status + + // should return the status of the tx. + getTxStatus (txId) { + const txMeta = this.getTx(txId) + return txMeta.status + } + + // should update the status of the tx to 'rejected'. + setTxStatusRejected (txId) { + this._setTxStatus(txId, 'rejected') + } + + // should update the status of the tx to 'unapproved'. + setTxStatusUnapproved (txId) { + this._setTxStatus(txId, 'unapproved') + } + // should update the status of the tx to 'approved'. + setTxStatusApproved (txId) { + this._setTxStatus(txId, 'approved') + } + + // should update the status of the tx to 'signed'. + setTxStatusSigned (txId) { + this._setTxStatus(txId, 'signed') + } + + // should update the status of the tx to 'submitted'. + setTxStatusSubmitted (txId) { + this._setTxStatus(txId, 'submitted') + } + + // should update the status of the tx to 'confirmed'. + setTxStatusConfirmed (txId) { + this._setTxStatus(txId, 'confirmed') + } + + setTxStatusFailed (txId, err) { + const txMeta = this.getTx(txId) + txMeta.err = { + message: err.toString(), + stack: err.stack, + } + this.updateTx(txMeta) + this._setTxStatus(txId, 'failed') + } + + wipeTransactions (address) { + // network only tx + const txs = this.getFullTxList() + const network = this.getNetwork() + + // Filter out the ones from the current account and network + const otherAccountTxs = txs.filter((txMeta) => !(txMeta.txParams.from === address && txMeta.metamaskNetworkId === network)) + + // Update state + this._saveTxList(otherAccountTxs) + } +// +// PRIVATE METHODS +// + + // Should find the tx in the tx list and + // update it. + // should set the status in txData + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'approved'` the user has approved the tx + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + // - `'failed'` the tx failed for some reason, included on tx data. + _setTxStatus (txId, status) { + const txMeta = this.getTx(txId) + txMeta.status = status + this.emit(`${txMeta.id}:${status}`, txId) + this.emit(`tx:status-update`, txId, status) + if (['submitted', 'rejected', 'failed'].includes(status)) { + this.emit(`${txMeta.id}:finished`, txMeta) + } + this.updateTx(txMeta, `txStateManager: setting status to ${status}`) + this.emit('update:badge') + } + + // Saves the new/updated txList. + // Function is intended only for internal use + _saveTxList (transactions) { + this.store.updateState({ transactions }) + } +} diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js deleted file mode 100644 index e8e23f8b5..000000000 --- a/app/scripts/lib/tx-utils.js +++ /dev/null @@ -1,136 +0,0 @@ -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 -const BN = ethUtil.BN - -/* -tx-utils are utility methods for Transaction manager -its passed a provider and that is passed to 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) - } - - analyzeGasUsage (txMeta, cb) { - var self = this - this.query.getBlockByNumber('latest', true, (err, block) => { - if (err) return cb(err) - async.waterfall([ - self.estimateTxGas.bind(self, txMeta, block.gasLimit), - self.setTxGas.bind(self, txMeta, block.gasLimit), - ], cb) - }) - } - - estimateTxGas (txMeta, blockGasLimitHex, cb) { - const txParams = txMeta.txParams - // check if gasLimit is already specified - txMeta.gasLimitSpecified = Boolean(txParams.gas) - // if not, fallback to block gasLimit - if (!txMeta.gasLimitSpecified) { - txParams.gas = blockGasLimitHex - } - // run tx, see if it will OOG - this.query.estimateGas(txParams, cb) - } - - setTxGas (txMeta, blockGasLimitHex, estimatedGasHex, cb) { - txMeta.estimatedGas = estimatedGasHex - const txParams = txMeta.txParams - - // if gasLimit was specified and doesnt OOG, - // use original specified amount - if (txMeta.gasLimitSpecified) { - txMeta.estimatedGas = txParams.gas - cb() - return - } - // if gasLimit not originally specified, - // try adding an additional gas buffer to our estimation for safety - const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex) - txParams.gas = recommendedGasHex - cb() - return - } - - addGasBuffer (initialGasLimitHex, blockGasLimitHex) { - const initialGasLimitBn = hexToBn(initialGasLimitHex) - const blockGasLimitBn = hexToBn(blockGasLimitHex) - const upperGasLimitBn = blockGasLimitBn.muln(0.9) - const bufferedGasLimitBn = initialGasLimitBn.muln(1.5) - - // if initialGasLimit is above blockGasLimit, dont modify it - if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn) - // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit - if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn) - // otherwise use blockGasLimit - return bnToHex(upperGasLimitBn) - } - - fillInTxParams (txParams, cb) { - let fromAddress = txParams.from - let 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) { - if (err) return cb(err) - // write results to txParams obj - Object.assign(txParams, result) - cb() - }) - } - - // builds ethTx from txParams object - buildEthTxFromParams (txParams) { - // normalize values - txParams.to = normalize(txParams.to) - txParams.from = normalize(txParams.from) - txParams.value = normalize(txParams.value) - txParams.data = normalize(txParams.data) - txParams.gas = normalize(txParams.gas || txParams.gasLimit) - txParams.gasPrice = normalize(txParams.gasPrice) - txParams.nonce = normalize(txParams.nonce) - // build ethTx - log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`) - const ethTx = new Transaction(txParams) - return ethTx - } - - publishTransaction (rawTx, cb) { - this.query.sendRawTransaction(rawTx, cb) - } - - validateTxParams (txParams, cb) { - if (('value' in txParams) && txParams.value.indexOf('-') === 0) { - cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) - } else { - cb() - } - } - - -} - -// util - -function isUndef(value) { - return value === undefined -} - -function bnToHex(inputBn) { - return ethUtil.addHexPrefix(inputBn.toString(16)) -} - -function hexToBn(inputHex) { - return new BN(ethUtil.stripHexPrefix(inputHex), 16) -} diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js new file mode 100644 index 000000000..8b760790e --- /dev/null +++ b/app/scripts/lib/typed-message-manager.js @@ -0,0 +1,123 @@ +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const createId = require('./random-id') +const assert = require('assert') +const sigUtil = require('eth-sig-util') + + +module.exports = class TypedMessageManager extends EventEmitter { + constructor (opts) { + super() + this.memStore = new ObservableStore({ + unapprovedTypedMessages: {}, + unapprovedTypedMessagesCount: 0, + }) + this.messages = [] + } + + get unapprovedTypedMessagesCount () { + return Object.keys(this.getUnapprovedMsgs()).length + } + + getUnapprovedMsgs () { + return this.messages.filter(msg => msg.status === 'unapproved') + .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + } + + addUnapprovedMessage (msgParams) { + this.validateParams(msgParams) + + log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) + // create txData obj with parameters and meta data + var time = (new Date()).getTime() + var msgId = createId() + var msgData = { + id: msgId, + msgParams: msgParams, + time: time, + status: 'unapproved', + type: 'eth_signTypedData', + } + this.addMsg(msgData) + + // signal update + this.emit('update') + return msgId + } + + validateParams (params) { + assert.equal(typeof params, 'object', 'Params should ben an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.ok(Array.isArray(params.data), 'Data should be an array.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.doesNotThrow(() => { + sigUtil.typedSignatureHash(params.data) + }, 'Expected EIP712 typed data') + } + + addMsg (msg) { + this.messages.push(msg) + this._saveMsgList() + } + + getMsg (msgId) { + return this.messages.find(msg => msg.id === msgId) + } + + approveMessage (msgParams) { + this.setMsgStatusApproved(msgParams.metamaskId) + return this.prepMsgForSigning(msgParams) + } + + setMsgStatusApproved (msgId) { + this._setMsgStatus(msgId, 'approved') + } + + setMsgStatusSigned (msgId, rawSig) { + const msg = this.getMsg(msgId) + msg.rawSig = rawSig + this._updateMsg(msg) + this._setMsgStatus(msgId, 'signed') + } + + prepMsgForSigning (msgParams) { + delete msgParams.metamaskId + return Promise.resolve(msgParams) + } + + rejectMsg (msgId) { + this._setMsgStatus(msgId, 'rejected') + } + + // + // PRIVATE METHODS + // + + _setMsgStatus (msgId, status) { + const msg = this.getMsg(msgId) + if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".') + msg.status = status + this._updateMsg(msg) + this.emit(`${msgId}:${status}`, msg) + if (status === 'rejected' || status === 'signed') { + this.emit(`${msgId}:finished`, msg) + } + } + + _updateMsg (msg) { + const index = this.messages.findIndex((message) => message.id === msg.id) + if (index !== -1) { + this.messages[index] = msg + } + this._saveMsgList() + } + + _saveMsgList () { + const unapprovedTypedMessages = this.getUnapprovedMsgs() + const unapprovedTypedMessagesCount = Object.keys(unapprovedTypedMessages).length + this.memStore.updateState({ unapprovedTypedMessages, unapprovedTypedMessagesCount }) + this.emit('updateBadge') + } + +} diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js new file mode 100644 index 000000000..6dee9edf0 --- /dev/null +++ b/app/scripts/lib/util.js @@ -0,0 +1,44 @@ +const ethUtil = require('ethereumjs-util') +const assert = require('assert') +const BN = require('bn.js') + +module.exports = { + getStack, + sufficientBalance, + hexToBn, + bnToHex, + BnMultiplyByFraction, +} + +function getStack () { + const stack = new Error('Stack trace generator - not an error').stack + return stack +} + +function sufficientBalance (txParams, hexBalance) { + // validate hexBalance is a hex string + assert.equal(typeof hexBalance, 'string', 'sufficientBalance - hexBalance is not a hex string') + assert.equal(hexBalance.slice(0, 2), '0x', 'sufficientBalance - hexBalance is not a hex string') + + const balance = hexToBn(hexBalance) + const value = hexToBn(txParams.value) + const gasLimit = hexToBn(txParams.gas) + const gasPrice = hexToBn(txParams.gasPrice) + + const maxCost = value.add(gasLimit.mul(gasPrice)) + return balance.gte(maxCost) +} + +function bnToHex (inputBn) { + return ethUtil.addHexPrefix(inputBn.toString(16)) +} + +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) +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2b8fc9cb8..ad4e71792 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1,38 +1,55 @@ const EventEmitter = require('events') const extend = require('xtend') -const promiseToCallback = require('promise-to-callback') -const pipe = require('pump') +const pump = 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 asStream = require('obs-store/lib/asStream') +const AccountTracker = require('./lib/account-tracker') +const RpcEngine = require('json-rpc-engine') +const debounce = require('debounce') +const createEngineStream = require('json-rpc-middleware-stream/engineStream') +const createFilterMiddleware = require('eth-json-rpc-filters') +const createOriginMiddleware = require('./lib/createOriginMiddleware') +const createLoggerMiddleware = require('./lib/createLoggerMiddleware') +const createProviderMiddleware = require('./lib/createProviderMiddleware') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex -const KeyringController = require('./keyring-controller') +const KeyringController = require('eth-keyring-controller') +const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const CurrencyController = require('./controllers/currency') const NoticeController = require('./notice-controller') const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') +const InfuraController = require('./controllers/infura') +const BlacklistController = require('./controllers/blacklist') +const RecentBlocksController = require('./controllers/recent-blocks') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') -const TxManager = require('./transaction-manager') +const TypedMessageManager = require('./lib/typed-message-manager') +const TransactionController = require('./controllers/transactions') +const BalancesController = require('./controllers/computed-balances') const ConfigManager = require('./lib/config-manager') -const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') - +const Mutex = require('await-semaphore').Mutex const version = require('../manifest.json').version +const BN = require('ethereumjs-util').BN +const GWEI_BN = new BN('1000000000') +const percentile = require('percentile') module.exports = class MetamaskController extends EventEmitter { constructor (opts) { super() + + this.defaultMaxListeners = 20 + + this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200) + this.opts = opts - let initState = opts.initState || {} + const initState = opts.initState || {} + this.recordFirstTimeInfo(initState) // platform-specific api this.platform = opts.platform @@ -40,8 +57,11 @@ module.exports = class MetamaskController extends EventEmitter { // observable state store this.store = new ObservableStore(initState) + // lock to ensure only one vault created at once + this.createVaultMutex = new Mutex() + // network store - this.networkStore = new ObservableStore({ network: 'loading' }) + this.networkController = new NetworkController(initState.NetworkController) // config manager this.configManager = new ConfigManager({ @@ -60,29 +80,47 @@ module.exports = class MetamaskController extends EventEmitter { this.currencyController.updateConversionRate() this.currencyController.scheduleConversionInterval() + // infura controller + this.infuraController = new InfuraController({ + initState: initState.InfuraController, + }) + this.infuraController.scheduleInfuraNetworkCheck() + + this.blacklistController = new BlacklistController() + this.blacklistController.scheduleUpdates() + // rpc provider this.provider = this.initializeProvider() - this.provider.on('block', this.logBlock.bind(this)) - this.provider.on('error', this.verifyNetwork.bind(this)) + this.blockTracker = this.provider._blockTracker - // eth data query tools - this.ethQuery = new EthQuery(this.provider) - this.ethStore = new EthStore({ + this.recentBlocksController = new RecentBlocksController({ + blockTracker: this.blockTracker, provider: this.provider, - blockTracker: this.provider, + }) + + // account tracker watches balances, nonces, and any code at their address. + this.accountTracker = new AccountTracker({ + provider: this.provider, + blockTracker: this.blockTracker, }) // key mgmt this.keyringController = new KeyringController({ initState: initState.KeyringController, - ethStore: this.ethStore, - getNetwork: this.getNetworkState.bind(this), + getNetwork: this.networkController.getNetworkState.bind(this.networkController), + encryptor: opts.encryptor || undefined, }) - this.keyringController.on('newAccount', (address) => { - this.preferencesController.setSelectedAddress(address) - }) - this.keyringController.on('newVault', (address) => { - autoFaucet(address) + + // If only one account exists, make sure it is selected. + this.keyringController.memStore.subscribe((state) => { + const addresses = state.keyrings.reduce((res, keyring) => { + return res.concat(keyring.accounts) + }, []) + if (addresses.length === 1) { + const address = addresses[0] + this.preferencesController.setSelectedAddress(address) + } + this.accountTracker.syncWithAddresses(addresses) }) // address book controller @@ -91,20 +129,35 @@ 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, + blockTracker: this.blockTracker, + getGasPrice: this.getGasPrice.bind(this), }) + this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts)) + + // computed balances (accounting for pending transactions) + this.balancesController = new BalancesController({ + accountTracker: this.accountTracker, + txController: this.txController, + blockTracker: this.blockTracker, + }) + this.networkController.on('networkDidChange', () => { + this.balancesController.updateAllBalances() + }) + this.balancesController.updateAllBalances() // notices this.noticeController = new NoticeController({ initState: initState.NoticeController, + version, + firstVersion: initState.firstTimeInfo.version, }) this.noticeController.updateNoticesList() // to be uncommented when retrieving notices from a remote server. @@ -114,14 +167,15 @@ module.exports = class MetamaskController extends EventEmitter { initState: initState.ShapeShiftController, }) - this.lookupNetwork() + this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() + this.typedMessageManager = new TypedMessageManager() 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,19 +195,31 @@ 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 }) + }) + + this.infuraController.store.subscribe((state) => { + this.store.updateState({ InfuraController: state }) + }) // manual mem state subscriptions - this.networkStore.subscribe(this.sendUpdate.bind(this)) - this.ethStore.subscribe(this.sendUpdate.bind(this)) - this.txManager.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)) - this.preferencesController.store.subscribe(this.sendUpdate.bind(this)) - this.addressBookController.store.subscribe(this.sendUpdate.bind(this)) - this.currencyController.store.subscribe(this.sendUpdate.bind(this)) - this.noticeController.memStore.subscribe(this.sendUpdate.bind(this)) - this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this)) + const sendUpdate = this.sendUpdate.bind(this) + this.networkController.store.subscribe(sendUpdate) + this.accountTracker.store.subscribe(sendUpdate) + this.txController.memStore.subscribe(sendUpdate) + this.balancesController.store.subscribe(sendUpdate) + this.messageManager.memStore.subscribe(sendUpdate) + this.personalMessageManager.memStore.subscribe(sendUpdate) + this.typedMessageManager.memStore.subscribe(sendUpdate) + this.keyringController.memStore.subscribe(sendUpdate) + this.preferencesController.store.subscribe(sendUpdate) + this.recentBlocksController.store.subscribe(sendUpdate) + this.addressBookController.store.subscribe(sendUpdate) + this.currencyController.store.subscribe(sendUpdate) + this.noticeController.memStore.subscribe(sendUpdate) + this.shapeshiftController.store.subscribe(sendUpdate) + this.infuraController.store.subscribe(sendUpdate) } // @@ -161,47 +227,50 @@ module.exports = class MetamaskController extends EventEmitter { // initializeProvider () { - - let provider = MetaMaskProvider({ + const providerOpts = { static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, - rpcUrl: this.configManager.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 - processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb), + processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), - - // new style msg signing + // personal_sign msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), - }) - return provider + processTypedMessage: this.newUnsignedTypedMessage.bind(this), + } + const providerProxy = this.networkController.initializeProvider(providerOpts) + return providerProxy } 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 } @@ -216,26 +285,32 @@ module.exports = class MetamaskController extends EventEmitter { const wallet = this.configManager.getWallet() const vault = this.keyringController.store.getState().vault const isInitialized = (!!wallet || !!vault) + return extend( { isInitialized, }, - this.networkStore.getState(), - this.ethStore.getState(), - this.txManager.memStore.getState(), + this.networkController.store.getState(), + this.accountTracker.store.getState(), + this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), + this.typedMessageManager.memStore.getState(), this.keyringController.memStore.getState(), + this.balancesController.store.getState(), this.preferencesController.store.getState(), this.addressBookController.store.getState(), this.currencyController.store.getState(), this.noticeController.memStore.getState(), + this.infuraController.store.getState(), + this.recentBlocksController.store.getState(), // config manager this.configManager.getConfig(), this.shapeshiftController.store.getState(), { lostAccounts: this.configManager.getLostAccounts(), seedWords: this.configManager.getSeedWords(), + forgottenPassword: this.configManager.getPasswordForgotten(), } ) } @@ -247,86 +322,122 @@ 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 + const networkController = this.networkController 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()), + setCurrentCurrency: this.setCurrentCurrency.bind(this), + setUseBlockie: this.setUseBlockie.bind(this), + markAccountsFound: this.markAccountsFound.bind(this), + markPasswordForgotten: this.markPasswordForgotten.bind(this), + unMarkPasswordForgotten: this.unMarkPasswordForgotten.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: nodeify(this.addNewAccount, this), + placeSeedWords: this.placeSeedWords.bind(this), + clearSeedWordCache: this.clearSeedWordCache.bind(this), + resetAccount: this.resetAccount.bind(this), + importAccountWithStrategy: this.importAccountWithStrategy.bind(this), // vault management - submitPassword: this.submitPassword.bind(this), + submitPassword: nodeify(keyringController.submitPassword, keyringController), + + // network management + setNetworkEndpoints: nodeify(networkController.setNetworkEndpoints, networkController), + setProviderType: nodeify(networkController.setProviderType, networkController), + setCustomRpc: nodeify(this.setCustomRpc, this), // PreferencesController - setSelectedAddress: nodeify(preferencesController.setSelectedAddress).bind(preferencesController), - setDefaultRpc: nodeify(this.setDefaultRpc).bind(this), - setCustomRpc: nodeify(this.setCustomRpc).bind(this), + setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), + addToken: nodeify(preferencesController.addToken, preferencesController), + removeToken: nodeify(preferencesController.removeToken, preferencesController), + setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), + setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), // AddressController - setAddressBook: nodeify(addressBookController.setAddressBook).bind(addressBookController), + setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), // 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), + setLocked: nodeify(keyringController.setLocked, keyringController), + createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), + createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this), + addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), + saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController), + exportAccount: nodeify(keyringController.exportAccount, keyringController), - // txManager - approveTransaction: txManager.approveTransaction.bind(txManager), - cancelTransaction: txManager.cancelTransaction.bind(txManager), - updateAndApproveTransaction: this.updateAndApproveTx.bind(this), + // txController + cancelTransaction: nodeify(txController.cancelTransaction, txController), + updateTransaction: nodeify(txController.updateTransaction, txController), + updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), + retryTransaction: nodeify(this.retryTransaction, this), // messageManager - signMessage: nodeify(this.signMessage).bind(this), - cancelMessage: this.cancelMessage.bind(this), + signMessage: nodeify(this.signMessage, this), + cancelMessage: this.cancelMessage.bind(this), // personalMessageManager - signPersonalMessage: nodeify(this.signPersonalMessage).bind(this), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this), + signPersonalMessage: nodeify(this.signPersonalMessage, this), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this), + + // personalMessageManager + signTypedMessage: nodeify(this.signTypedMessage, this), + cancelTypedMessage: this.cancelTypedMessage.bind(this), // notices - checkNotices: noticeController.updateNoticesList.bind(noticeController), + checkNotices: noticeController.updateNoticesList.bind(noticeController), markNoticeRead: noticeController.markNoticeRead.bind(noticeController), } } setupUntrustedCommunication (connectionStream, originDomain) { + // Check if new connection is blacklisted + if (this.blacklistController.checkForPhishing(originDomain)) { + log.debug('MetaMask - sending phishing warning for', originDomain) + this.sendPhishingWarning(connectionStream, originDomain) + return + } + // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mux = setupMultiplex(connectionStream) // connect features - this.setupProviderConnection(mx.createStream('provider'), originDomain) - this.setupPublicConfig(mx.createStream('publicConfig')) + this.setupProviderConnection(mux.createStream('provider'), originDomain) + this.setupPublicConfig(mux.createStream('publicConfig')) } setupTrustedCommunication (connectionStream, originDomain) { // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mux = setupMultiplex(connectionStream) // connect features - this.setupControllerConnection(mx.createStream('controller')) - this.setupProviderConnection(mx.createStream('provider'), originDomain) + this.setupControllerConnection(mux.createStream('controller')) + this.setupProviderConnection(mux.createStream('provider'), originDomain) + } + + sendPhishingWarning (connectionStream, hostname) { + const mux = setupMultiplex(connectionStream) + const phishingStream = mux.createStream('phishing') + phishingStream.write({ hostname }) } setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) - outStream.pipe(dnode).pipe(outStream) + pump( + outStream, + dnode, + outStream, + (err) => { + if (err) log.error(err) + } + ) dnode.on('remote', (remote) => { // push updates to popup const sendUpdate = remote.sendUpdate.bind(remote) @@ -334,49 +445,141 @@ module.exports = class MetamaskController extends EventEmitter { }) } - setupProviderConnection (outStream, originDomain) { - streamIntoProvider(outStream, this.provider, logger) - function logger (err, request, response) { - if (err) return console.error(err) - if (response.error) { - console.error('Error in RPC response:\n', response.error) - } - if (request.isMetamaskInternal) return - if (global.METAMASK_DEBUG) { - console.log(`RPC (${originDomain}):`, request, '->', response) - } - } - } + setupProviderConnection (outStream, origin) { + // setup json rpc engine stack + const engine = new RpcEngine() - setupPublicConfig (outStream) { - pipe( - this.publicConfigStore, - outStream + // create filter polyfill middleware + const filterMiddleware = createFilterMiddleware({ + provider: this.provider, + blockTracker: this.provider._blockTracker, + }) + + engine.push(createOriginMiddleware({ origin })) + engine.push(createLoggerMiddleware({ origin })) + engine.push(filterMiddleware) + engine.push(createProviderMiddleware({ provider: this.provider })) + + // setup connection + const providerStream = createEngineStream({ engine }) + pump( + outStream, + providerStream, + outStream, + (err) => { + // cleanup filter polyfill middleware + filterMiddleware.destroy() + if (err) log.error(err) + } ) } - sendUpdate () { + setupPublicConfig (outStream) { + pump( + asStream(this.publicConfigStore), + outStream, + (err) => { + if (err) log.error(err) + } + ) + } + + privateSendUpdate () { this.emit('update', this.getState()) } + getGasPrice () { + const { recentBlocksController } = this + const { recentBlocks } = recentBlocksController.store.getState() + + // Return 1 gwei if no blocks have been observed: + if (recentBlocks.length === 0) { + return '0x' + GWEI_BN.toString(16) + } + + const lowestPrices = recentBlocks.map((block) => { + if (!block.gasPrices || block.gasPrices.length < 1) { + return GWEI_BN + } + return block.gasPrices + .map(hexPrefix => hexPrefix.substr(2)) + .map(hex => new BN(hex, 16)) + .sort((a, b) => { + return a.gt(b) ? 1 : -1 + })[0] + }) + .map(number => number.div(GWEI_BN).toNumber()) + + const percentileNum = percentile(50, lowestPrices) + const percentileNumBn = new BN(percentileNum) + return '0x' + percentileNumBn.mul(GWEI_BN).toString(16) + } + // // Vault Management // - submitPassword (password, cb) { - return this.keyringController.submitPassword(password) - .then((newState) => { cb(null, newState) }) - .catch((reason) => { cb(reason) }) + async createNewVaultAndKeychain (password) { + const release = await this.createVaultMutex.acquire() + let vault + + try { + const accounts = await this.keyringController.getAccounts() + + if (accounts.length > 0) { + vault = await this.keyringController.fullUpdate() + + } else { + vault = await this.keyringController.createNewVaultAndKeychain(password) + this.selectFirstIdentity(vault) + } + release() + } catch (err) { + release() + throw err + } + + return vault + } + + async createNewVaultAndRestore (password, seed) { + const release = await this.createVaultMutex.acquire() + try { + const vault = await this.keyringController.createNewVaultAndRestore(password, seed) + this.selectFirstIdentity(vault) + release() + return vault + } catch (err) { + release() + throw err + } + } + + selectFirstIdentity (vault) { + const { identities } = vault + const address = Object.keys(identities)[0] + this.preferencesController.setSelectedAddress(address) } // // Opinionated Keyring Management // - addNewAccount (cb) { + async addNewAccount (cb) { const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] if (!primaryKeyring) return cb(new Error('MetamaskController - No HD Key Tree found')) - promiseToCallback(this.keyringController.addNewAccount(primaryKeyring))(cb) + const keyringController = this.keyringController + const oldAccounts = await keyringController.getAccounts() + const keyState = await keyringController.addNewAccount(primaryKeyring) + const newAccounts = await keyringController.getAccounts() + + newAccounts.forEach((address) => { + if (!oldAccounts.includes(address)) { + this.preferencesController.setSelectedAddress(address) + } + }) + + return keyState } // Adds the current vault's seed words to the UI's state tree. @@ -403,6 +606,13 @@ module.exports = class MetamaskController extends EventEmitter { cb(null, this.preferencesController.getSelectedAddress()) } + resetAccount (cb) { + const selectedAddress = this.preferencesController.getSelectedAddress() + this.txController.wipeTransactions(selectedAddress) + cb(null, selectedAddress) + } + + importAccountWithStrategy (strategy, args, cb) { accountImporter.importAccount(strategy, args) .then((privateKey) => { @@ -418,30 +628,17 @@ module.exports = class MetamaskController extends EventEmitter { // // Identity Management // + // - newUnapprovedTransaction (txParams, cb) { - log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) - const self = this - self.txManager.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) => { - switch (completedTx.status) { - case 'submitted': - return cb(null, completedTx.hash) - case 'rejected': - return cb(new Error('MetaMask Tx Signature: User denied transaction signature.')) - default: - return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) - } - }) - }) + async retryTransaction (txId, cb) { + await this.txController.retryTransaction(txId) + const state = await this.getState() + return state } + 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 +658,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 +673,26 @@ module.exports = class MetamaskController extends EventEmitter { }) } - updateAndApproveTx(txMeta, cb) { - log.debug(`MetaMaskController - updateAndApproveTx: ${JSON.stringify(txMeta)}`) - const txManager = this.txManager - txManager.updateTx(txMeta) - txManager.approveTransaction(txMeta.id, cb) + newUnsignedTypedMessage (msgParams, cb) { + let msgId + try { + msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + } catch (e) { + return cb(e) + } + + this.typedMessageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) } signMessage (msgParams, cb) { @@ -502,7 +714,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 +724,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 +757,25 @@ module.exports = class MetamaskController extends EventEmitter { }) } - cancelPersonalMessage(msgId, cb) { + signTypedMessage (msgParams) { + log.info('MetaMaskController - signTypedMessage') + const msgId = msgParams.metamaskId + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.typedMessageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signTypedMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.typedMessageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) + } + + cancelPersonalMessage (msgId, cb) { const messageManager = this.personalMessageManager messageManager.rejectMsg(msgId) if (cb && typeof cb === 'function') { @@ -553,19 +783,39 @@ module.exports = class MetamaskController extends EventEmitter { } } + cancelTypedMessage (msgId, cb) { + const messageManager = this.typedMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + markAccountsFound (cb) { this.configManager.setLostAccounts([]) this.sendUpdate() cb(null, this.getState()) } - restoreOldVaultAccounts(migratorOutput) { + markPasswordForgotten(cb) { + this.configManager.setPasswordForgotten(true) + this.sendUpdate() + cb() + } + + unMarkPasswordForgotten(cb) { + this.configManager.setPasswordForgotten(false) + this.sendUpdate() + cb() + } + + 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 +841,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 +859,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 }) } @@ -624,70 +868,30 @@ module.exports = class MetamaskController extends EventEmitter { this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) } - // // network - // - verifyNetwork () { - // Check network when restoring connectivity: - if (this.isNetworkLoading()) this.lookupNetwork() + async setCustomRpc (rpcTarget, rpcList) { + this.networkController.setRpcTarget(rpcTarget) + await this.preferencesController.updateFrequentRpcList(rpcTarget) + return rpcTarget } - setDefaultRpc () { - this.configManager.setRpcTarget('http://localhost:8545') - this.platform.reload() - this.lookupNetwork() - return Promise.resolve('http://localhost:8545') - } - - setCustomRpc (rpcTarget, rpcList) { - this.configManager.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') + setUseBlockie (val, cb) { + try { + this.preferencesController.setUseBlockie(val) + cb(null) + } catch (err) { + cb(err) } + } - this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { - if (err) { - this.setNetworkState('loading') - return + recordFirstTimeInfo (initState) { + if (!('firstTimeInfo' in initState)) { + initState.firstTimeInfo = { + version, + date: Date.now(), } - if (global.METAMASK_DEBUG) { - console.log('web3.getNetwork returned ' + network) - } - this.setNetworkState(network) - }) + } } } diff --git a/app/scripts/migrations/002.js b/app/scripts/migrations/002.js index 36a870342..b1d88f2ef 100644 --- a/app/scripts/migrations/002.js +++ b/app/scripts/migrations/002.js @@ -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') { diff --git a/app/scripts/migrations/003.js b/app/scripts/migrations/003.js index 1893576ad..140f81d40 100644 --- a/app/scripts/migrations/003.js +++ b/app/scripts/migrations/003.js @@ -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) { diff --git a/app/scripts/migrations/004.js b/app/scripts/migrations/004.js index 405d932f8..cd558300c 100644 --- a/app/scripts/migrations/004.js +++ b/app/scripts/migrations/004.js @@ -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) diff --git a/app/scripts/migrations/005.js b/app/scripts/migrations/005.js index e4b84f460..f7b68dfe4 100644 --- a/app/scripts/migrations/005.js +++ b/app/scripts/migrations/005.js @@ -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 diff --git a/app/scripts/migrations/006.js b/app/scripts/migrations/006.js index 94d1b6ecd..51ea6e3e7 100644 --- a/app/scripts/migrations/006.js +++ b/app/scripts/migrations/006.js @@ -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 diff --git a/app/scripts/migrations/007.js b/app/scripts/migrations/007.js index 236e35224..d9887b9c8 100644 --- a/app/scripts/migrations/007.js +++ b/app/scripts/migrations/007.js @@ -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 diff --git a/app/scripts/migrations/008.js b/app/scripts/migrations/008.js index cd5e95d22..da7cb2e60 100644 --- a/app/scripts/migrations/008.js +++ b/app/scripts/migrations/008.js @@ -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 diff --git a/app/scripts/migrations/009.js b/app/scripts/migrations/009.js index 4612fefdc..f47db55ac 100644 --- a/app/scripts/migrations/009.js +++ b/app/scripts/migrations/009.js @@ -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 diff --git a/app/scripts/migrations/010.js b/app/scripts/migrations/010.js index c0cc56ae4..e4b9ac07e 100644 --- a/app/scripts/migrations/010.js +++ b/app/scripts/migrations/010.js @@ -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 diff --git a/app/scripts/migrations/011.js b/app/scripts/migrations/011.js index 0d5d6d307..782ec809d 100644 --- a/app/scripts/migrations/011.js +++ b/app/scripts/migrations/011.js @@ -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 diff --git a/app/scripts/migrations/012.js b/app/scripts/migrations/012.js index 8361b3793..f69ccbb02 100644 --- a/app/scripts/migrations/012.js +++ b/app/scripts/migrations/012.js @@ -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 diff --git a/app/scripts/migrations/013.js b/app/scripts/migrations/013.js new file mode 100644 index 000000000..8f11e510e --- /dev/null +++ b/app/scripts/migrations/013.js @@ -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 +} diff --git a/app/scripts/migrations/014.js b/app/scripts/migrations/014.js new file mode 100644 index 000000000..0fe92125b --- /dev/null +++ b/app/scripts/migrations/014.js @@ -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 +} diff --git a/app/scripts/migrations/015.js b/app/scripts/migrations/015.js new file mode 100644 index 000000000..4b839580b --- /dev/null +++ b/app/scripts/migrations/015.js @@ -0,0 +1,38 @@ +const version = 15 + +/* + +This migration sets transactions with the 'Gave up submitting tx.' err message +to a 'failed' stated + +*/ + +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 + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + if (!txMeta.err) return txMeta + else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed' + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/016.js b/app/scripts/migrations/016.js new file mode 100644 index 000000000..4fc534f1c --- /dev/null +++ b/app/scripts/migrations/016.js @@ -0,0 +1,41 @@ +const version = 16 + +/* + +This migration sets transactions with the 'Gave up submitting tx.' err message +to a 'failed' stated + +*/ + +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 + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + if (!txMeta.err) return txMeta + if (txMeta.err === 'transaction with the same hash was already imported.') { + txMeta.status = 'submitted' + delete txMeta.err + } + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/017.js b/app/scripts/migrations/017.js new file mode 100644 index 000000000..24959cd3a --- /dev/null +++ b/app/scripts/migrations/017.js @@ -0,0 +1,40 @@ +const version = 17 + +/* + +This migration sets transactions who were retried and marked as failed to submitted + +*/ + +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 + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + if (!txMeta.status === 'failed') return txMeta + if (txMeta.retryCount > 0 && txMeta.retryCount < 2) { + txMeta.status = 'submitted' + delete txMeta.err + } + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js new file mode 100644 index 000000000..d27fe3f46 --- /dev/null +++ b/app/scripts/migrations/018.js @@ -0,0 +1,52 @@ +const version = 18 + +/* + +This migration updates "transaction state history" to diffs style + +*/ + +const clone = require('clone') +const txStateHistoryHelper = require('../lib/tx-state-history-helper') + + +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 + const transactions = newState.TransactionController.transactions + newState.TransactionController.transactions = transactions.map((txMeta) => { + // no history: initialize + if (!txMeta.history || txMeta.history.length === 0) { + const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta) + txMeta.history = [snapshot] + return txMeta + } + // has history: migrate + const newHistory = ( + txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history) + // remove empty diffs + .filter((entry) => { + return !Array.isArray(entry) || entry.length > 0 + }) + ) + txMeta.history = newHistory + return txMeta + }) + return newState +} diff --git a/app/scripts/migrations/019.js b/app/scripts/migrations/019.js new file mode 100644 index 000000000..072c96370 --- /dev/null +++ b/app/scripts/migrations/019.js @@ -0,0 +1,83 @@ + +const version = 19 + +/* + +This migration sets transactions as failed +whos nonce is too high + +*/ + +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 + const transactions = newState.TransactionController.transactions + + newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => { + if (txMeta.status !== 'submitted') return txMeta + + const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed') + .filter((tx) => tx.txParams.from === txMeta.txParams.from) + .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from) + const highestConfirmedNonce = getHighestNonce(confirmedTxs) + + const pendingTxs = txList.filter((tx) => tx.status === 'submitted') + .filter((tx) => tx.txParams.from === txMeta.txParams.from) + .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from) + const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce) + + const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce) + + if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) { + txMeta.status = 'failed' + txMeta.err = { + message: 'nonce too high', + note: 'migration 019 custom error', + } + } + return txMeta + }) + return newState +} + +function getHighestContinuousFrom (txList, startPoint) { + const nonces = txList.map((txMeta) => { + const nonce = txMeta.txParams.nonce + return parseInt(nonce, 16) + }) + + let highest = startPoint + while (nonces.includes(highest)) { + highest++ + } + + return highest +} + +function getHighestNonce (txList) { + const nonces = txList.map((txMeta) => { + const nonce = txMeta.txParams.nonce + return parseInt(nonce || '0x0', 16) + }) + const highestNonce = Math.max.apply(null, nonces) + return highestNonce +} + diff --git a/app/scripts/migrations/020.js b/app/scripts/migrations/020.js new file mode 100644 index 000000000..8159b3e70 --- /dev/null +++ b/app/scripts/migrations/020.js @@ -0,0 +1,41 @@ +const version = 20 + +/* + +This migration ensures previous installations +get a `firstTimeInfo` key on the metamask state, +so that we can version notices in the future. + +*/ + +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 ('metamask' in newState && + !('firstTimeInfo' in newState.metamask)) { + newState.metamask.firstTimeInfo = { + version: '3.12.0', + date: Date.now(), + } + } + return newState +} + diff --git a/app/scripts/migrations/021.js b/app/scripts/migrations/021.js new file mode 100644 index 000000000..d84e77b50 --- /dev/null +++ b/app/scripts/migrations/021.js @@ -0,0 +1,34 @@ +const version = 21 + +/* + +This migration removes the BlackListController from disk state + +*/ + +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 + delete newState.BlacklistController + delete newState.RecentBlocks + return newState +} + diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js index 04c966d4d..7a4578ea7 100644 --- a/app/scripts/migrations/_multi-keyring.js +++ b/app/scripts/migrations/_multi-keyring.js @@ -10,20 +10,20 @@ which we dont have access to at the time of this writing. const ObservableStore = require('obs-store') const ConfigManager = require('../../app/scripts/lib/config-manager') const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator') -const KeyringController = require('../../app/scripts/lib/keyring-controller') +const KeyringController = require('eth-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) }) }) - }, } diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 019b4d13d..a0cf5f4d4 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -23,4 +23,13 @@ module.exports = [ require('./010'), require('./011'), require('./012'), + require('./013'), + require('./014'), + require('./015'), + require('./016'), + require('./017'), + require('./018'), + require('./019'), + require('./020'), + require('./021'), ] diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js index 57aad40c5..14a63eae7 100644 --- a/app/scripts/notice-controller.js +++ b/app/scripts/notice-controller.js @@ -1,13 +1,17 @@ const EventEmitter = require('events').EventEmitter +const semver = require('semver') const extend = require('xtend') const ObservableStore = require('obs-store') const hardCodedNotices = require('../../notices/notices.json') +const uniqBy = require('lodash.uniqby') module.exports = class NoticeController extends EventEmitter { constructor (opts) { super() this.noticePoller = null + this.firstVersion = opts.firstVersion + this.version = opts.version const initState = extend({ noticesList: [], }, opts.initState) @@ -30,9 +34,9 @@ module.exports = class NoticeController extends EventEmitter { return unreadNotices[unreadNotices.length - 1] } - setNoticesList (noticesList) { + async setNoticesList (noticesList) { this.store.updateState({ noticesList }) - return Promise.resolve(true) + return true } markNoticeRead (noticeToMark, cb) { @@ -50,12 +54,14 @@ module.exports = class NoticeController extends EventEmitter { } } - updateNoticesList () { - return this._retrieveNoticeData().then((newNotices) => { - var oldNotices = this.getNoticesList() - var combinedNotices = this._mergeNotices(oldNotices, newNotices) - return Promise.resolve(this.setNoticesList(combinedNotices)) - }) + async updateNoticesList () { + const newNotices = await this._retrieveNoticeData() + const oldNotices = this.getNoticesList() + const combinedNotices = this._mergeNotices(oldNotices, newNotices) + const filteredNotices = this._filterNotices(combinedNotices) + const result = this.setNoticesList(filteredNotices) + this._updateMemstore() + return result } startPolling () { @@ -68,22 +74,30 @@ module.exports = class NoticeController extends EventEmitter { } _mergeNotices (oldNotices, newNotices) { - var noticeMap = this._mapNoticeIds(oldNotices) - newNotices.forEach((notice) => { - if (noticeMap.indexOf(notice.id) === -1) { - oldNotices.push(notice) + return uniqBy(oldNotices.concat(newNotices), 'id') + } + + _filterNotices (notices) { + return notices.filter((newNotice) => { + if ('version' in newNotice) { + const satisfied = semver.satisfies(this.version, newNotice.version) + return satisfied } + if ('firstVersion' in newNotice) { + const satisfied = semver.satisfies(this.firstVersion, newNotice.firstVersion) + return satisfied + } + return true }) - return oldNotices } _mapNoticeIds (notices) { return notices.map((notice) => notice.id) } - _retrieveNoticeData () { + async _retrieveNoticeData () { // Placeholder for the API. - return Promise.resolve(hardCodedNotices) + return hardCodedNotices } _updateMemstore () { diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 00c2aa275..f5cc255d1 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -5,7 +5,6 @@ class ExtensionPlatform { // // Public // - reload () { extension.runtime.reload() } @@ -18,6 +17,20 @@ class ExtensionPlatform { return extension.runtime.getManifest().version } + openExtensionInBrowser () { + const extensionURL = extension.runtime.getURL('home.html') + this.openWindow({ url: extensionURL }) + } + + getPlatformInfo (cb) { + try { + extension.runtime.getPlatformInfo((platform) => { + cb(null, platform) + }) + } catch (e) { + cb(e) + } + } } module.exports = ExtensionPlatform diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 1e5d70e8b..2e4334bb1 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -1,7 +1,8 @@ const EventEmitter = require('events').EventEmitter const async = require('async') const Dnode = require('dnode') -const Web3 = require('web3') +const Eth = require('ethjs') +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 +17,6 @@ function initializePopup ({ container, connectionStream }, cb) { (cb) => connectToAccountManager(connectionStream, cb), (accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb), ], cb) - } function connectToAccountManager (connectionStream, cb) { @@ -33,7 +33,9 @@ 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) + global.eth = new Eth(providerStream) } function setupControllerConnection (connectionStream, cb) { diff --git a/app/scripts/popup.js b/app/scripts/popup.js index 0fbde54b3..53ab00e00 100644 --- a/app/scripts/popup.js +++ b/app/scripts/popup.js @@ -1,5 +1,6 @@ const injectCss = require('inject-css') -const MetaMaskUiCss = require('../../ui/css') +const OldMetaMaskUiCss = require('../../old-ui/css') +const NewMetaMaskUiCss = require('../../ui/css') const startPopup = require('./popup-core') const PortStream = require('./lib/port-stream.js') const isPopupOrNotification = require('./lib/is-popup-or-notification') @@ -7,13 +8,18 @@ const extension = require('extensionizer') const ExtensionPlatform = require('./platforms/extension') const NotificationManager = require('./lib/notification-manager') const notificationManager = new NotificationManager() +const setupRaven = require('./setupRaven') // create platform global global.platform = new ExtensionPlatform() +// setup sentry error reporting +const release = global.platform.getVersion() +setupRaven({ release }) + // inject css -const css = MetaMaskUiCss() -injectCss(css) +// const css = MetaMaskUiCss() +// injectCss(css) // identify window type (popup, notification) const windowType = isPopupOrNotification() @@ -28,8 +34,30 @@ const connectionStream = new PortStream(extensionPort) const container = document.getElementById('app-content') startPopup({ container, connectionStream }, (err, store) => { if (err) return displayCriticalError(err) + + // Code commented out until we begin auto adding users to NewUI + // const { isMascara, identities = {}, featureFlags = {} } = store.getState().metamask + // const firstTime = Object.keys(identities).length === 0 + const { isMascara, featureFlags = {} } = store.getState().metamask + let betaUIState = featureFlags.betaUI + + // Code commented out until we begin auto adding users to NewUI + // const useBetaCss = isMascara || firstTime || betaUIState + const useBetaCss = isMascara || betaUIState + + let css = useBetaCss ? NewMetaMaskUiCss() : OldMetaMaskUiCss() + let deleteInjectedCss = injectCss(css) + let newBetaUIState + store.subscribe(() => { const state = store.getState() + newBetaUIState = state.metamask.featureFlags.betaUI + if (newBetaUIState !== betaUIState) { + deleteInjectedCss() + betaUIState = newBetaUIState + css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss() + deleteInjectedCss = injectCss(css) + } if (state.appState.shouldClose) notificationManager.closePopup() }) }) @@ -41,7 +69,7 @@ function closePopupIfOpen (windowType) { } } -function displayCriticalError(err) { +function displayCriticalError (err) { container.innerHTML = '
The MetaMask app failed to load: please open and close MetaMask again to restart.
' container.style.height = '80px' log.error(err.stack) diff --git a/app/scripts/setupRaven.js b/app/scripts/setupRaven.js new file mode 100644 index 000000000..7beffeff9 --- /dev/null +++ b/app/scripts/setupRaven.js @@ -0,0 +1,26 @@ +const Raven = require('./vendor/raven.min.js') +const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' +const PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505' +const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' + +module.exports = setupRaven + +// Setup raven / sentry remote error reporting +function setupRaven(opts) { + const { release } = opts + let ravenTarget + + if (METAMASK_DEBUG) { + console.log('Setting up Sentry Remote Error Reporting: DEV') + ravenTarget = DEV + } else { + console.log('Setting up Sentry Remote Error Reporting: PROD') + ravenTarget = PROD + } + + Raven.config(ravenTarget, { + release, + }).install() + + return Raven +} diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js deleted file mode 100644 index d7051b2cb..000000000 --- a/app/scripts/transaction-manager.js +++ /dev/null @@ -1,404 +0,0 @@ -const EventEmitter = require('events') -const async = require('async') -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') - -module.exports = class TransactionManager extends EventEmitter { - constructor (opts) { - super() - this.store = new ObservableStore(extend({ - transactions: [], - }, opts.initState)) - this.memStore = new ObservableStore({}) - this.networkStore = opts.networkStore || new ObservableStore({}) - this.preferencesStore = opts.preferencesStore || new ObservableStore({}) - 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.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() ) - } - - getState () { - return this.memStore.getState() - } - - getNetwork () { - return this.networkStore.getState().network - } - - getSelectedAddress () { - return this.preferencesStore.getState().selectedAddress - } - - // Returns the tx list - getTxList () { - let network = this.getNetwork() - let fullTxList = this.getFullTxList() - return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) - } - - // Returns the number of txs for the current network. - getTxCount () { - return this.getTxList().length - } - - // Returns the full tx list across all networks - getFullTxList () { - return this.store.getState().transactions - } - - // Adds a tx to the txlist - addTx (txMeta) { - let txCount = this.getTxCount() - let network = this.getNetwork() - let fullTxList = this.getFullTxList() - let txHistoryLimit = this.txHistoryLimit - - // checks if the length of the tx history is - // longer then desired persistence limit - // and then if it is removes only confirmed - // or rejected tx's. - // not tx's that are pending or unapproved - if (txCount > txHistoryLimit - 1) { - var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) - fullTxList.splice(index, 1) - } - fullTxList.push(txMeta) - this._saveTxList(fullTxList) - this.emit('update') - - this.once(`${txMeta.id}:signed`, function (txId) { - this.removeAllListeners(`${txMeta.id}:rejected`) - }) - this.once(`${txMeta.id}:rejected`, function (txId) { - this.removeAllListeners(`${txMeta.id}:signed`) - }) - - this.emit('updateBadge') - this.emit(`${txMeta.id}:unapproved`, txMeta) - } - - // gets tx by Id and returns it - getTx (txId, cb) { - var txList = this.getTxList() - var txMeta = txList.find(txData => txData.id === txId) - return cb ? cb(txMeta) : txMeta - } - - // - updateTx (txMeta) { - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') - } - - get unapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - get pendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length - } - - addUnapprovedTransaction (txParams, done) { - let txMeta - async.waterfall([ - // validate - (cb) => this.txProviderUtils.validateTxParams(txParams, cb), - // construct txMeta - (cb) => { - txMeta = { - id: createId(), - time: (new Date()).getTime(), - status: 'unapproved', - metamaskNetworkId: this.getNetwork(), - txParams: txParams, - } - cb() - }, - // add default tx params - (cb) => this.addTxDefaults(txMeta, cb), - // save txMeta - (cb) => { - this.addTx(txMeta) - cb(null, txMeta) - }, - ], done) - } - - addTxDefaults (txMeta, cb) { - const txParams = txMeta.txParams - // ensure value - txParams.value = txParams.value || '0x0' - this.query.gasPrice((err, gasPrice) => { - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) - }) - } - - getUnapprovedTxList () { - var txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) - } - - approveTransaction (txId, cb = warn) { - const self = this - // approve - self.setTxStatusApproved(txId) - // only allow one tx at a time for atomic nonce usage - self.nonceLock.take(() => { - // begin signature process - async.waterfall([ - (cb) => self.fillInTxParams(txId, cb), - (cb) => self.signTransaction(txId, cb), - (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), - ], (err) => { - self.nonceLock.leave() - if (err) { - this.setTxStatusFailed(txId, { - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) - return cb(err) - } - cb() - }) - }) - } - - cancelTransaction (txId, cb = warn) { - this.setTxStatusRejected(txId) - cb() - } - - fillInTxParams (txId, cb) { - let txMeta = this.getTx(txId) - this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { - if (err) return cb(err) - this.updateTx(txMeta) - cb() - }) - } - - getChainId() { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState.network) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } - } - - signTransaction (txId, cb) { - const txMeta = this.getTx(txId) - const txParams = txMeta.txParams - const fromAddress = txParams.from - // add network/chain id - txParams.chainId = this.getChainId() - const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - this.signEthTx(ethTx, fromAddress).then(() => { - this.setTxStatusSigned(txMeta.id) - cb(null, ethUtil.bufferToHex(ethTx.serialize())) - }).catch((err) => { - cb(err) - }) - } - - publishTransaction (txId, rawTx, cb) { - this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) return cb(err) - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - cb() - }) - } - - // 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) - txMeta.hash = txHash - this.updateTx(txMeta) - } - - /* - Takes an object of fields to search for eg: - var thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - } - and returns a list of tx with all - options matching - - this is for things like filtering a the tx list - for only tx's from 1 account - or for filltering for all txs from one account - and that have been 'confirmed' - */ - getFilteredTxList (opts) { - var filteredTxList - Object.keys(opts).forEach((key) => { - filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) - }) - return filteredTxList - } - - getTxsByMetaData (key, value, txList = this.getTxList()) { - return txList.filter((txMeta) => { - if (txMeta.txParams[key]) { - return txMeta.txParams[key] === value - } else { - return txMeta[key] === value - } - }) - } - - // STATUS METHODS - // get::set status - - // should return the status of the tx. - getTxStatus (txId) { - const txMeta = this.getTx(txId) - return txMeta.status - } - - // should update the status of the tx to 'rejected'. - setTxStatusRejected (txId) { - this._setTxStatus(txId, 'rejected') - } - - // should update the status of the tx to 'approved'. - setTxStatusApproved (txId) { - this._setTxStatus(txId, 'approved') - } - - // should update the status of the tx to 'signed'. - setTxStatusSigned (txId) { - this._setTxStatus(txId, 'signed') - } - - // should update the status of the tx to 'submitted'. - setTxStatusSubmitted (txId) { - this._setTxStatus(txId, 'submitted') - } - - // should update the status of the tx to 'confirmed'. - setTxStatusConfirmed (txId) { - this._setTxStatus(txId, 'confirmed') - } - - setTxStatusFailed (txId, reason) { - let txMeta = this.getTx(txId) - txMeta.err = reason - this.updateTx(txMeta) - this._setTxStatus(txId, 'failed') - } - - // merges txParams obj onto txData.txParams - // use extend to ensure that all fields are filled - updateTxParams (txId, txParams) { - var txMeta = this.getTx(txId) - txMeta.txParams = extend(txMeta.txParams, txParams) - this.updateTx(txMeta) - } - - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' - checkForTxInBlock () { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id - if (!txHash) { - let errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) - } - this.query.getTransactionByHash(txHash, (err, txParams) => { - if (err || !txParams) { - if (!txParams) return - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - return console.error(err) - } - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) - } - }) - }) - } - - // PRIVATE METHODS - - // Should find the tx in the tx list and - // update it. - // should set the status in txData - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - _setTxStatus (txId, status) { - var txMeta = this.getTx(txId) - txMeta.status = status - this.emit(`${txMeta.id}:${status}`, txId) - if (status === 'submitted' || status === 'rejected') { - this.emit(`${txMeta.id}:finished`, txMeta) - } - this.updateTx(txMeta) - this.emit('updateBadge') - } - - // Saves the new/updated txList. - // Function is intended only for internal use - _saveTxList (transactions) { - this.store.updateState({ transactions }) - } - - _updateMemstore () { - const unapprovedTxs = this.getUnapprovedTxList() - const selectedAddressTxList = this.getFilteredTxList({ - from: this.getSelectedAddress(), - metamaskNetworkId: this.getNetwork(), - }) - this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) - } -} - - -const warn = () => console.warn('warn was used no cb provided') diff --git a/app/scripts/vendor/raven.min.js b/app/scripts/vendor/raven.min.js new file mode 100644 index 000000000..b439aeae6 --- /dev/null +++ b/app/scripts/vendor/raven.min.js @@ -0,0 +1,3 @@ +/*! Raven.js 3.22.1 (7584197) | github.com/getsentry/raven-js */ +!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.Raven=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;gthis.k.maxBreadcrumbs&&this.u.shift(),this},addPlugin:function(a){var b=[].slice.call(arguments,1);return this.r.push([a,b]),this.n&&this.E(),this},setUserContext:function(a){return this.j.user=a,this},setExtraContext:function(a){return this.T("extra",a),this},setTagsContext:function(a){return this.T("tags",a),this},clearContext:function(){return this.j={},this},getContext:function(){return JSON.parse(h(this.j))},setEnvironment:function(a){return this.k.environment=a,this},setRelease:function(a){return this.k.release=a,this},setDataCallback:function(a){var b=this.k.dataCallback;return this.k.dataCallback=e(b,a),this},setBreadcrumbCallback:function(a){var b=this.k.breadcrumbCallback;return this.k.breadcrumbCallback=e(b,a),this},setShouldSendCallback:function(a){var b=this.k.shouldSendCallback;return this.k.shouldSendCallback=e(b,a),this},setTransport:function(a){return this.k.transport=a,this},lastException:function(){return this.d},lastEventId:function(){return this.f},isSetup:function(){return!!this.a&&(!!this.g||(this.ravenNotConfiguredError||(this.ravenNotConfiguredError=!0,this.z("error","Error: Raven has not been configured.")),!1))},afterLoad:function(){var a=J.RavenConfig;a&&this.config(a.dsn,a.config).install()},showReportDialog:function(a){if(K){a=a||{};var b=a.eventId||this.lastEventId();if(!b)throw new i("Missing eventId");var c=a.dsn||this.G;if(!c)throw new i("Missing DSN");var d=encodeURIComponent,e="";e+="?eventId="+d(b),e+="&dsn="+d(c);var f=a.user||this.j.user;f&&(f.name&&(e+="&name="+d(f.name)),f.email&&(e+="&email="+d(f.email)));var g=this.I(this.F(c)),h=K.createElement("script");h.async=!0,h.src=g+"/api/embed/error-page/"+e,(K.head||K.body).appendChild(h)}},K:function(){var a=this;this.m+=1,setTimeout(function(){a.m-=1})},U:function(a,b){var c,d;if(this.b){b=b||{},a="raven"+a.substr(0,1).toUpperCase()+a.substr(1),K.createEvent?(c=K.createEvent("HTMLEvents"),c.initEvent(a,!0,!0)):(c=K.createEventObject(),c.eventType=a);for(d in b)w(b,d)&&(c[d]=b[d]);if(K.createEvent)K.dispatchEvent(c);else try{K.fireEvent("on"+c.eventType.toLowerCase(),c)}catch(e){}}},V:function(a){var b=this;return function(c){if(b.W=null,b.v!==c){b.v=c;var d;try{d=A(c.target)}catch(e){d=""}b.captureBreadcrumb({category:"ui."+a,message:d})}}},X:function(){var a=this,b=1e3;return function(c){var d;try{d=c.target}catch(e){return}var f=d&&d.tagName;if(f&&("INPUT"===f||"TEXTAREA"===f||d.isContentEditable)){var g=a.W;g||a.V("input")(c),clearTimeout(g),a.W=setTimeout(function(){a.W=null},b)}}},Y:function(a,b){var c=D(this.w.href),d=D(b),e=D(a);this.x=b,c.protocol===d.protocol&&c.host===d.host&&(b=d.relative),c.protocol===e.protocol&&c.host===e.host&&(a=e.relative),this.captureBreadcrumb({category:"navigation",data:{to:b,from:a}})},B:function(){var a=this;a.Z=Function.prototype.toString,Function.prototype.toString=function(){return"function"==typeof this&&this.L?a.Z.apply(this.N,arguments):a.Z.apply(this,arguments)}},O:function(){this.Z&&(Function.prototype.toString=this.Z)},C:function(){function a(a){return function(b,d){for(var e=new Array(arguments.length),f=0;f2?arguments[2]:void 0;return c&&b.Y(b.x,c+""),a.apply(this,arguments)}};E(history,"pushState",j,d),E(history,"replaceState",j,d)}if(c.console&&"console"in J&&console.log){var k=function(a,c){b.captureBreadcrumb({message:a,level:c.level,category:"console"})};s(["debug","info","warn","error","log"],function(a,b){G(console,b,k)})}},P:function(){for(var a;this.t.length;){a=this.t.shift();var b=a[0],c=a[1],d=a[2];b[c]=d}},E:function(){var a=this;s(this.r,function(b,c){var d=c[0],e=c[1];d.apply(a,[a].concat(e))})},F:function(a){var b=I.exec(a),c={},d=7;try{for(;d--;)c[H[d]]=b[d]||""}catch(e){throw new i("Invalid DSN: "+a)}if(c.pass&&!this.k.allowSecretKey)throw new i("Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key");return c},I:function(a){var b="//"+a.host+(a.port?":"+a.port:"");return a.protocol&&(b=a.protocol+":"+b),b},A:function(){this.m||this.Q.apply(this,arguments)},Q:function(a,b){var c=this.R(a,b);this.U("handle",{stackInfo:a,options:b}),this._(a.name,a.message,a.url,a.lineno,c,b)},R:function(a,b){var c=this,d=[];if(a.stack&&a.stack.length&&(s(a.stack,function(b,e){var f=c.aa(e,a.url);f&&d.push(f)}),b&&b.trimHeadFrames))for(var e=0;e0&&(a.breadcrumbs={values:[].slice.call(this.u,0)}),this.j.user&&(a.user=this.j.user),b.environment&&(a.environment=b.environment),b.release&&(a.release=b.release),b.serverName&&(a.server_name=b.serverName),Object.keys(a).forEach(function(b){(null==a[b]||""===a[b]||r(a[b]))&&delete a[b]}),o(b.dataCallback)&&(a=b.dataCallback(a)||a),a&&!r(a)&&(!o(b.shouldSendCallback)||b.shouldSendCallback(a)))return this.ga()?void this.z("warn","Raven dropped error due to backoff: ",a):void("number"==typeof b.sampleRate?Math.random() ",i=h.length;a&&f++1&&g+e.length*i+b.length>=d));)e.push(b),g+=b.length,a=a.parentNode;return e.reverse().join(h)}function y(a){var b,c,d,e,f,g=[];if(!a||!a.tagName)return"";if(g.push(a.tagName.toLowerCase()),a.id&&g.push("#"+a.id),b=a.className,b&&i(b))for(c=b.split(/\s+/),f=0;f=0;--b)s[b]===a&&s.splice(b,1)}function c(){n(),s=[]}function k(a,b){var c=null;if(!b||f.collectWindowErrors){for(var d in s)if(s.hasOwnProperty(d))try{s[d].apply(null,[a].concat(h.call(arguments,2)))}catch(e){c=e}if(c)throw c}}function l(a,b,c,g,h){var l=null,m=e.isErrorEvent(h)?h.error:h,n=e.isErrorEvent(a)?a.message:a;if(v)f.computeStackTrace.augmentStackTraceWithInitialElement(v,b,c,n),o();else if(m&&e.isError(m))l=f.computeStackTrace(m),k(l,!0);else{var p,r={url:b,line:c,column:g},s=void 0;if("[object String]"==={}.toString.call(n)){var p=n.match(j);p&&(s=p[1],n=p[2])}r.func=i,l={name:s,message:n,url:d(),stack:[r]},k(l,!0)}return!!q&&q.apply(this,arguments)}function m(){r||(q=g.onerror,g.onerror=l,r=!0)}function n(){r&&(g.onerror=q,r=!1,q=void 0)}function o(){var a=v,b=t;t=null,v=null,u=null,k.apply(null,[a,!1].concat(b))}function p(a,b){var c=h.call(arguments,1);if(v){if(u===a)return;o()}var d=f.computeStackTrace(a);if(v=d,u=a,t=c,setTimeout(function(){u===a&&o()},d.incomplete?2e3:0),b!==!1)throw a}var q,r,s=[],t=null,u=null,v=null;return p.subscribe=a,p.unsubscribe=b,p.uninstall=c,p}(),f.computeStackTrace=function(){function a(a){if("undefined"!=typeof a.stack&&a.stack){for(var b,c,e,f=/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,g=/^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i,h=/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx(?:-web)|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,j=/(\S+) line (\d+)(?: > eval line \d+)* > eval/i,k=/\((\S*)(?::(\d+))(?::(\d+))\)/,l=a.stack.split("\n"),m=[],n=(/^(.*) is undefined$/.exec(a.message),0),o=l.length;n eval")>-1;q&&(b=j.exec(c[3]))?(c[3]=b[1],c[4]=b[2],c[5]=null):0!==n||c[5]||"undefined"==typeof a.columnNumber||(m[0].column=a.columnNumber+1),e={url:c[3],func:c[1]||i,args:c[2]?c[2].split(","):[],line:c[4]?+c[4]:null,column:c[5]?+c[5]:null}}!e.func&&e.line&&(e.func=i),m.push(e)}return m.length?{name:a.name,message:a.message,url:d(),stack:m}:null}}function b(a,b,c,d){var e={url:b,line:c};if(e.url&&e.line){if(a.incomplete=!1,e.func||(e.func=i),a.stack.length>0&&a.stack[0].url===e.url){if(a.stack[0].line===e.line)return!1;if(!a.stack[0].line&&a.stack[0].func===e.func)return a.stack[0].line=e.line,!1}return a.stack.unshift(e),a.partial=!0,!0}return a.incomplete=!0,!1}function c(a,g){for(var h,j,k=/function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i,l=[],m={},n=!1,o=c.caller;o&&!n;o=o.caller)if(o!==e&&o!==f.report){if(j={url:null,func:i,line:null,column:null},o.name?j.func=o.name:(h=k.exec(o.toString()))&&(j.func=h[1]),"undefined"==typeof j.func)try{j.func=h.input.substring(0,h.input.indexOf("{"))}catch(p){}m[""+o]?n=!0:m[""+o]=!0,l.push(j)}g&&l.splice(0,g);var q={name:a.name,message:a.message,url:d(),stack:l};return b(q,a.sourceURL||a.fileName,a.line||a.lineNumber,a.message||a.description),q}function e(b,e){var g=null;e=null==e?0:+e;try{if(g=a(b))return g}catch(h){if(f.debug)throw h}try{if(g=c(b,e+1))return g}catch(h){if(f.debug)throw h}return{name:b.name,message:b.message,url:d()}}return e.augmentStackTraceWithInitialElement=b,e.computeStackTraceFromStackProp=a,e}(),b.exports=f}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{5:5}],7:[function(a,b,c){function d(a,b){for(var c=0;c0){var i=d(c,this);~i?c.splice(i+1):c.push(this),~i?e.splice(i,1/0,g):e.push(g),~d(c,h)&&(h=b.call(this,g,h))}else c.push(h);return null==a?h instanceof Error?f(h):h:a.call(this,g,h)}}c=b.exports=e,c.getSerialize=g},{}]},{},[4])(4)}); +//# sourceMappingURL=raven.min.js.map \ No newline at end of file diff --git a/circle.yml b/circle.yml index c9ea787ff..6aba5c1be 100644 --- a/circle.yml +++ b/circle.yml @@ -1,7 +1,17 @@ machine: node: - version: 6.0.0 + version: 8.1.4 +test: + override: + - "npm test" dependencies: pre: - - "npm i -g testem" - - "npm i -g mocha" + - sudo apt-get update + # get latest stable firefox + - sudo apt-get install firefox + - firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd + # get latest stable chrome + - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' + - sudo apt-get update + - sudo apt-get install google-chrome-stable \ No newline at end of file diff --git a/development/announcer.js b/development/announcer.js index 110d41fd4..e97ea65b6 100644 --- a/development/announcer.js +++ b/development/announcer.js @@ -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! It should auto-update soon!\n${log}` console.log(msg) diff --git a/development/backGroundConnectionModifiers.js b/development/backGroundConnectionModifiers.js new file mode 100644 index 000000000..ffbe49d4d --- /dev/null +++ b/development/backGroundConnectionModifiers.js @@ -0,0 +1,26 @@ +module.exports = { + "confirm sig requests": { + signMessage: (msgData, cb) => { + const stateUpdate = { + unapprovedMsgs: {}, + unapprovedMsgCount: 0, + } + return cb(null, stateUpdate) + }, + signPersonalMessage: (msgData, cb) => { + const stateUpdate = { + unapprovedPersonalMsgs: {}, + unapprovedPersonalMsgsCount: 0, + } + return cb(null, stateUpdate) + }, + signTypedMessage: (msgData, cb) => { + const stateUpdate = { + unapprovedTypedMessages: {}, + unapprovedTypedMessagesCount: 0, + } + return cb(null, stateUpdate) + }, + }, +} + diff --git a/development/index.html b/development/index.html index 048aa3f35..e5a027447 100644 --- a/development/index.html +++ b/development/index.html @@ -3,62 +3,58 @@ MetaMask - - - -
+ + + + - - - - diff --git a/development/mockExtension.js b/development/mockExtension.js index 55799b2bf..ac03d965c 100644 --- a/development/mockExtension.js +++ b/development/mockExtension.js @@ -37,3 +37,8 @@ apis.forEach(function (api) { extension.runtime.reload = noop extension.tabs.create = noop +extension.runtime.getManifest = function () { + return { + version: 'development' + } +} \ No newline at end of file diff --git a/development/selector.js b/development/selector.js index c466905ca..fd387df15 100644 --- a/development/selector.js +++ b/development/selector.js @@ -11,7 +11,14 @@ function NewComponent () { NewComponent.prototype.render = function () { const props = this.props - let { states, selectedKey, actions, store } = props + let { + states, + selectedKey, + actions, + store, + modifyBackgroundConnection, + backGroundConnectionModifiers, + } = props const state = this.state || {} const selected = state.selected || selectedKey @@ -23,6 +30,8 @@ NewComponent.prototype.render = function () { value: selected, onChange:(event) => { const selectedKey = event.target.value + const backgroundConnectionModifier = backGroundConnectionModifiers[selectedKey] + modifyBackgroundConnection(backgroundConnectionModifier || {}) store.dispatch(actions.update(selectedKey)) this.setState({ selected: selectedKey }) }, diff --git a/development/states/add-token.json b/development/states/add-token.json new file mode 100644 index 000000000..e78393b7f --- /dev/null +++ b/development/states/add-token.json @@ -0,0 +1,132 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "featureFlags": {"betaUI": true}, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Send Account 1" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "name": "Send Account 2" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "name": "Send Account 3" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "address": "0xd85a4b6a394794842887b8284293d69163007bbb", + "name": "Send Account 4" + } + }, + "unapprovedTxs": {}, + "conversionRate": 1200.88200327, + "conversionDate": 1489013762, + "noActiveNotices": true, + "frequentRpcList": [], + "network": "3", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "code": "0x", + "balance": "0x47c9d71831c76efe", + "nonce": "0x1b", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "code": "0x", + "balance": "0x37452b1315889f80", + "nonce": "0xa", + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "code": "0x", + "balance": "0x30c9d71831c76efe", + "nonce": "0x1c", + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0xd85a4b6a394794842887b8284293d69163007bbb" + } + }, + "addressBook": [ + { + "address": "0x06195827297c7a80a443b6894d3bdb8824b43896", + "name": "Address Book Account 1" + } + ], + "tokens": [], + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + }, + { + "type": "Simple Key Pair", + "accounts": [ + "0xd85a4b6a394794842887b8284293d69163007bbb" + ] + } + ], + "selectedAddress": "0xd85a4b6a394794842887b8284293d69163007bbb", + "currentCurrency": "USD", + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "send": { + "gasLimit": null, + "gasPrice": null, + "gasTotal": "0xb451dc41b578", + "tokenBalance": null, + "from": "", + "to": "", + "amount": "0x0", + "memo": "", + "errors": {}, + "maxModeOn": false, + "editingTransactionId": null + } + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "detailView": null, + "context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" + }, + "accountDetail": { + "subview": "transactions" + }, + "modal": { + "modalState": {}, + "previousModalState": {} + }, + "transForward": true, + "isLoading": false, + "warning": null, + "scrollToBottom": false, + "forgottenPassword": null + }, + "identities": {} +} diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json new file mode 100644 index 000000000..6ea8e64cd --- /dev/null +++ b/development/states/confirm-new-ui.json @@ -0,0 +1,154 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "featureFlags": {"betaUI": true}, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Send Account 1" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "name": "Send Account 2" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "name": "Send Account 3" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "address": "0xd85a4b6a394794842887b8284293d69163007bbb", + "name": "Send Account 4" + } + }, + "unapprovedTxs": {}, + "currentCurrency": "USD", + "conversionRate": 1200.88200327, + "conversionDate": 1489013762, + "noActiveNotices": true, + "frequentRpcList": [], + "network": "3", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "code": "0x", + "balance": "0x47c9d71831c76efe", + "nonce": "0x1b", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "code": "0x", + "balance": "0x37452b1315889f80", + "nonce": "0xa", + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "code": "0x", + "balance": "0x30c9d71831c76efe", + "nonce": "0x1c", + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0xd85a4b6a394794842887b8284293d69163007bbb" + } + }, + "addressBook": [ + { + "address": "0x06195827297c7a80a443b6894d3bdb8824b43896", + "name": "Address Book Account 1" + } + ], + "tokens": [], + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedTxs": { + "4768706228115573": { + "id": 4768706228115573, + "time": 1487363153561, + "status": "unapproved", + "gasMultiplier": 1, + "metamaskNetworkId": "3", + "txParams": { + "from": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "to": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "value": "0x1bc16d674ec80000", + "metamaskId": 4768706228115573, + "metamaskNetworkId": "3", + "gas": "0xea60", + "gasPrice": "0xba43b7400" + } + } + }, + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + }, + { + "type": "Simple Key Pair", + "accounts": [ + "0xd85a4b6a394794842887b8284293d69163007bbb" + ] + } + ], + "selectedAddress": "0xd85a4b6a394794842887b8284293d69163007bbb", + "currentCurrency": "USD", + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "send": { + "gasLimit": "0xea60", + "gasPrice": "0xba43b7400", + "gasTotal": "0xb451dc41b578", + "tokenBalance": null, + "from": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "balance": "0x37452b1315889f80" + }, + "to": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "amount": "0x1bc16d674ec80000", + "memo": "", + "errors": {}, + "maxModeOn": false, + "editingTransactionId": null + } + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "confTx", + "detailView": null, + "context": 0 + }, + "accountDetail": { + "subview": "transactions" + }, + "modal": { + "modalState": {}, + "previousModalState": {} + }, + "transForward": true, + "isLoading": false, + "warning": null, + "scrollToBottom": false, + "forgottenPassword": null + }, + "identities": {} +} diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json new file mode 100644 index 000000000..0a691e948 --- /dev/null +++ b/development/states/confirm-sig-requests.json @@ -0,0 +1,175 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "featureFlags": {"betaUI": true}, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Send Account 1" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "name": "Send Account 2" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "name": "Send Account 3" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "address": "0xd85a4b6a394794842887b8284293d69163007bbb", + "name": "Send Account 4" + } + }, + "unapprovedTxs": {}, + "currentCurrency": "USD", + "conversionRate": 1200.88200327, + "conversionDate": 1489013762, + "noActiveNotices": true, + "frequentRpcList": [], + "network": "3", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "code": "0x", + "balance": "0x47c9d71831c76efe", + "nonce": "0x1b", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "code": "0x", + "balance": "0x37452b1315889f80", + "nonce": "0xa", + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "code": "0x", + "balance": "0x30c9d71831c76efe", + "nonce": "0x1c", + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0xd85a4b6a394794842887b8284293d69163007bbb" + } + }, + "addressBook": [ + { + "address": "0x06195827297c7a80a443b6894d3bdb8824b43896", + "name": "Address Book Account 1" + } + ], + "tokens": [], + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedTxs": {}, + "unapprovedMsgs": { + "8927167822566864": { + "id": 8927167822566864, + "msgParams": { + "data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0", + "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658" + }, + "status": "unapproved", + "time": 1537889069339, + "type": "eth_sign" + } + }, + "unapprovedMsgCount": 1, + "unapprovedPersonalMsgs": { + "8907167822566865": { + "id": 8907167822566865, + "msgParams": { + "data": "0x23205465726d73206f662055736520230a0a2a2a544849532041475245454d454e54204953205355424a45435420544f2042494e44494e47204152424954524154494f4e20414e44204120574149564552204f4620434c41535320414354494f4e205249474854532041532044455441494c454420494e2053454354494f4e2031332e20504c454153452052454144205448452041475245454d454e54204341524546554c4c592e2a2a0a0a5f4f7572205465726d73206f66205573652068617665206265656e2075706461746564206173206f662053657074656d62657220352c20323031365f0a0a232320312e20416363657074616e6365206f66205465726d732023230a0a4d6574614d61736b2070726f7669646573206120706c6174666f726d20666f72206d616e6167696e6720457468657265756d20286f7220224554482229206163636f756e74732c20616e6420616c6c6f77696e67206f7264696e61727920776562736974657320746f20696e74657261637420776974682074686520457468657265756d20626c6f636b636861696e2c207768696c65206b656570696e6720746865207573657220696e20636f6e74726f6c206f7665722077686174207472616e73616374696f6e73207468657920617070726f76652c207468726f756768206f75722077656273697465206c6f63617465642061745b205d28687474703a2f2f6d6574616d61736b2e696f295b68747470733a2f2f6d6574616d61736b2e696f2f5d2868747470733a2f2f6d6574616d61736b2e696f2f2920616e642062726f7773657220706c7567696e2028746865202253697465222920e2809420776869636820696e636c7564657320746578742c20696d616765732c20617564696f2c20636f646520616e64206f74686572206d6174657269616c73202028636f6c6c6563746976656c792c2074686520e2809c436f6e74656e74e2809d2920616e6420616c6c206f66207468652066656174757265732c20616e642073657276696365732070726f76696465642e2054686520536974652c20616e6420616e79206f746865722066656174757265732c20746f6f6c732c206d6174657269616c732c206f72206f74686572207365727669636573206f6666657265642066726f6d2074696d6520746f2074696d65206279204d6574614d61736b2061726520726566657272656420746f20686572652061732074686520e2809c536572766963652ee2809d20506c656173652072656164207468657365205465726d73206f6620557365202874686520e2809c5465726d73e2809d206f7220e2809c5465726d73206f6620557365e2809d29206361726566756c6c79206265666f7265207573696e672074686520536572766963652e204279207573696e67206f72206f746865727769736520616363657373696e67207468652053657276696365732c206f7220636c69636b696e6720746f20616363657074206f7220616772656520746f207468657365205465726d732077686572652074686174206f7074696f6e206973206d61646520617661696c61626c652c20796f75202831292061636365707420616e6420616772656520746f207468657365205465726d732028322920636f6e73656e7420746f2074686520636f6c6c656374696f6e2c207573652c20646973636c6f7375726520616e64206f746865722068616e646c696e67206f6620696e666f726d6174696f6e2061732064657363726962656420696e206f7572205072697661637920506f6c6963792020616e642028332920616e79206164646974696f6e616c207465726d732c2072756c657320616e6420636f6e646974696f6e73206f662070617274696369706174696f6e20697373756564206279204d6574614d61736b2066726f6d2074696d6520746f2074696d652e20496620796f7520646f206e6f7420616772656520746f20746865205465726d732c207468656e20796f75206d6179206e6f7420616363657373206f72207573652074686520436f6e74656e74206f722053657276696365732e0a0a232320322e204d6f64696669636174696f6e206f66205465726d73206f66205573652023230a0a45786365707420666f722053656374696f6e2031332c2070726f766964696e6720666f722062696e64696e67206172626974726174696f6e20616e6420776169766572206f6620636c61737320616374696f6e207269676874732c204d6574614d61736b207265736572766573207468652072696768742c2061742069747320736f6c652064697363726574696f6e2c20746f206d6f64696679206f72207265706c61636520746865205465726d73206f662055736520617420616e792074696d652e20546865206d6f73742063757272656e742076657273696f6e206f66207468657365205465726d732077696c6c20626520706f73746564206f6e206f757220536974652e20596f75207368616c6c20626520726573706f6e7369626c6520666f7220726576696577696e6720616e64206265636f6d696e672066616d696c696172207769746820616e792073756368206d6f64696669636174696f6e732e20557365206f662074686520536572766963657320627920796f7520616674657220616e79206d6f64696669636174696f6e20746f20746865205465726d7320636f6e737469747574657320796f757220616363657074616e6365206f6620746865205465726d73206f6620557365206173206d6f6469666965642e0a0a0a0a232320332e20456c69676962696c6974792023230a0a596f752068657265627920726570726573656e7420616e642077617272616e74207468617420796f75206172652066756c6c792061626c6520616e6420636f6d706574656e7420746f20656e74657220696e746f20746865207465726d732c20636f6e646974696f6e732c206f626c69676174696f6e732c2061666669726d6174696f6e732c20726570726573656e746174696f6e7320616e642077617272616e746965732073657420666f72746820696e207468657365205465726d7320616e6420746f20616269646520627920616e6420636f6d706c792077697468207468657365205465726d732e0a0a4d6574614d61736b206973206120676c6f62616c20706c6174666f726d20616e6420627920616363657373696e672074686520436f6e74656e74206f722053657276696365732c20796f752061726520726570726573656e74696e6720616e642077617272616e74696e6720746861742c20796f7520617265206f6620746865206c6567616c20616765206f66206d616a6f7269747920696e20796f7572206a7572697364696374696f6e20617320697320726571756972656420746f20616363657373207375636820536572766963657320616e6420436f6e74656e…16e79206368616e67657320746f20746869732073656374696f6e2e204368616e6765732077696c6c206265636f6d6520656666656374697665206f6e207468652036307468206461792c20616e642077696c6c206170706c792070726f73706563746976656c79206f6e6c7920746f20616e7920636c61696d732061726973696e67206166746572207468652036307468206461792e0a0a466f7220616e792064697370757465206e6f74207375626a65637420746f206172626974726174696f6e20796f7520616e64204d6574614d61736b20616772656520746f207375626d697420746f2074686520706572736f6e616c20616e64206578636c7573697665206a7572697364696374696f6e206f6620616e642076656e756520696e20746865206665646572616c20616e6420737461746520636f75727473206c6f636174656420696e204e657720596f726b2c204e657720596f726b2e20596f75206675727468657220616772656520746f206163636570742073657276696365206f662070726f63657373206279206d61696c2c20616e642068657265627920776169766520616e7920616e6420616c6c206a7572697364696374696f6e616c20616e642076656e756520646566656e736573206f746865727769736520617661696c61626c652e0a0a546865205465726d7320616e64207468652072656c6174696f6e73686970206265747765656e20796f7520616e64204d6574614d61736b207368616c6c20626520676f7665726e656420627920746865206c617773206f6620746865205374617465206f66204e657720596f726b20776974686f75742072656761726420746f20636f6e666c696374206f66206c61772070726f766973696f6e732e0a0a23232031342e2047656e6572616c20496e666f726d6174696f6e2023230a0a2323232031342e3120456e746972652041677265656d656e74202323230a0a5468657365205465726d732028616e6420616e79206164646974696f6e616c207465726d732c2072756c657320616e6420636f6e646974696f6e73206f662070617274696369706174696f6e2074686174204d6574614d61736b206d617920706f7374206f6e2074686520536572766963652920636f6e737469747574652074686520656e746972652061677265656d656e74206265747765656e20796f7520616e64204d6574614d61736b2077697468207265737065637420746f20746865205365727669636520616e64207375706572736564657320616e79207072696f722061677265656d656e74732c206f72616c206f72207772697474656e2c206265747765656e20796f7520616e64204d6574614d61736b2e20496e20746865206576656e74206f66206120636f6e666c696374206265747765656e207468657365205465726d7320616e6420746865206164646974696f6e616c207465726d732c2072756c657320616e6420636f6e646974696f6e73206f662070617274696369706174696f6e2c20746865206c61747465722077696c6c207072657661696c206f76657220746865205465726d7320746f2074686520657874656e74206f662074686520636f6e666c6963742e0a0a2323232031342e322057616976657220616e642053657665726162696c697479206f66205465726d73202323230a0a546865206661696c757265206f66204d6574614d61736b20746f206578657263697365206f7220656e666f72636520616e79207269676874206f722070726f766973696f6e206f6620746865205465726d73207368616c6c206e6f7420636f6e73746974757465206120776169766572206f662073756368207269676874206f722070726f766973696f6e2e20496620616e792070726f766973696f6e206f6620746865205465726d7320697320666f756e6420627920616e2061726269747261746f72206f7220636f757274206f6620636f6d706574656e74206a7572697364696374696f6e20746f20626520696e76616c69642c207468652070617274696573206e657665727468656c6573732061677265652074686174207468652061726269747261746f72206f7220636f7572742073686f756c6420656e646561766f7220746f20676976652065666665637420746f2074686520706172746965732720696e74656e74696f6e73206173207265666c656374656420696e207468652070726f766973696f6e2c20616e6420746865206f746865722070726f766973696f6e73206f6620746865205465726d732072656d61696e20696e2066756c6c20666f72636520616e64206566666563742e0a0a2323232031342e332053746174757465206f66204c696d69746174696f6e73202323230a0a596f752061677265652074686174207265676172646c657373206f6620616e792073746174757465206f72206c617720746f2074686520636f6e74726172792c20616e7920636c61696d206f72206361757365206f6620616374696f6e2061726973696e67206f7574206f66206f722072656c6174656420746f2074686520757365206f66207468652053657276696365206f7220746865205465726d73206d7573742062652066696c65642077697468696e206f6e65202831292079656172206166746572207375636820636c61696d206f72206361757365206f6620616374696f6e2061726f7365206f7220626520666f7265766572206261727265642e0a0a2323232031342e342053656374696f6e205469746c6573202323230a0a5468652073656374696f6e207469746c657320696e20746865205465726d732061726520666f7220636f6e76656e69656e6365206f6e6c7920616e642068617665206e6f206c6567616c206f7220636f6e747261637475616c206566666563742e0a0a2323232031342e3520436f6d6d756e69636174696f6e73202323230a0a55736572732077697468207175657374696f6e732c20636f6d706c61696e7473206f7220636c61696d732077697468207265737065637420746f207468652053657276696365206d617920636f6e74616374207573207573696e67207468652072656c6576616e7420636f6e7461637420696e666f726d6174696f6e2073657420666f7274682061626f766520616e6420617420636f6d6d756e69636174696f6e73406d6574616d61736b2e696f2e0a0a23232031352052656c61746564204c696e6b732023230a0a2a2a5b5465726d73206f66205573655d2868747470733a2f2f6d6574616d61736b2e696f2f7465726d732e68746d6c292a2a0a0a2a2a5b507269766163795d2868747470733a2f2f6d6574616d61736b2e696f2f707269766163792e68746d6c292a2a0a0a2a2a5b4174747269627574696f6e735d2868747470733a2f2f6d6574616d61736b2e696f2f6174747269627574696f6e732e68746d6c292a2a0a", + "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659" + }, + "status": "unapproved", + "time": 1517889069339, + "type": "personal_sign" + } + }, + "unapprovedPersonalMsgCount": 0, + "unapprovedTypedMessages": { + "8997167822566869": { + "id": 8997167822566869, + "msgParams": { + "data": [ + {"type": "string", "name": "Message", "value": "Hi, Alice!"}, + {"type": "uint32", "name": "A number", "value": "1337"} + ], + "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659" + }, + "status": "unapproved", + "time": 1617889069339, + "type": "eth_signTypedData" + } + }, + "unapprovedTypedMessagesCount": 1, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + }, + { + "type": "Simple Key Pair", + "accounts": [ + "0xd85a4b6a394794842887b8284293d69163007bbb" + ] + } + ], + "selectedAddress": "0xd85a4b6a394794842887b8284293d69163007bbb", + "currentCurrency": "USD", + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "send": { + "gasLimit": "0xea60", + "gasPrice": "0xba43b7400", + "gasTotal": "0xb451dc41b578", + "tokenBalance": null, + "from": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "balance": "0x37452b1315889f80" + }, + "to": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "amount": "0x1bc16d674ec80000", + "memo": "", + "errors": {}, + "maxModeOn": false, + "editingTransactionId": null + } + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "confTx", + "detailView": null, + "context": 0 + }, + "accountDetail": { + "subview": "transactions" + }, + "modal": { + "modalState": {}, + "previousModalState": {} + }, + "transForward": true, + "isLoading": false, + "warning": null, + "scrollToBottom": false, + "forgottenPassword": null + }, + "identities": {} +} diff --git a/development/states/first-time.json b/development/states/first-time.json index 683a61fdf..4f5352992 100644 --- a/development/states/first-time.json +++ b/development/states/first-time.json @@ -4,9 +4,11 @@ "isUnlocked": false, "rpcTarget": "https://rawtestrpc.metamask.io/", "identities": {}, + "computedBalances": {}, "frequentRpcList": [], "unapprovedTxs": {}, "currentCurrency": "USD", + "featureFlags": {"betaUI": false}, "conversionRate": 12.7527416, "conversionDate": 1487624341, "noActiveNotices": false, @@ -33,7 +35,8 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [] + "lostAccounts": [], + "tokens": [] }, "appState": { "menuOpen": false, @@ -46,7 +49,13 @@ }, "transForward": true, "isLoading": false, - "warning": null + "warning": null, + "modal": { + "modalState": {"name": null}, + "open": false, + "previousModalState": {"name": null} + } }, - "identities": {} + "identities": {}, + "computedBalances": {} } diff --git a/development/states/pending-tx.json b/development/states/pending-tx.json new file mode 100644 index 000000000..bfa93f7ae --- /dev/null +++ b/development/states/pending-tx.json @@ -0,0 +1,739 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "isMascara": false, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Account 1" + } + }, + "unapprovedTxs": {}, + "noActiveNotices": true, + "frequentRpcList": [ + "http://192.168.1.34:7545/" + ], + "addressBook": [], + "tokenExchangeRates": {}, + "coinOptions": {}, + "provider": { + "type": "mainnet", + "rpcTarget": "https://mainnet.infura.io/metamask" + }, + "network": "1", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "code": "0x", + "balance": "0x1b3f641ed0c2f62", + "nonce": "0x35", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + } + }, + "currentBlockGasLimit": "0x66df83", + "selectedAddressTxList": [ + { + "id": 3516145537630216, + "time": 1512615655535, + "status": "submitted", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xc1b710800", + "gas": "0x7b0c", + "nonce": "0x35", + "chainId": "0x1" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208", + "history": [ + { + "id": 3516145537630216, + "time": 1512615655535, + "status": "unapproved", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xe6f7cec00", + "gas": "0x7b0c" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208" + }, + [ + { + "op": "replace", + "path": "/txParams/gasPrice", + "value": "0xc1b710800", + "note": "confTx: user approved transaction" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "approved", + "note": "txStateManager: setting status to approved" + } + ], + [ + { + "op": "add", + "path": "/txParams/nonce", + "value": "0x35", + "note": "transactions#approveTransaction" + }, + { + "op": "add", + "path": "/nonceDetails", + "value": { + "params": { + "highestLocalNonce": 53, + "highestSuggested": 53, + "nextNetworkNonce": 53 + }, + "local": { + "name": "local", + "nonce": 53, + "details": { + "startPoint": 53, + "highest": 53 + } + }, + "network": { + "name": "network", + "nonce": 53, + "details": { + "baseCount": 53 + } + } + } + } + ], + [ + { + "op": "add", + "path": "/txParams/chainId", + "value": "0x1", + "note": "txStateManager: setting status to signed" + }, + { + "op": "replace", + "path": "/status", + "value": "signed" + } + ], + [ + { + "op": "add", + "path": "/rawTx", + "value": "0xf86c35850c1b710800827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0f5142ba79a13ca7ec65548953017edafb217803244bbf9821d9ad077d89921e9a03afcb614169c90be9905d5b469d06984825c76675d3a535937cdb8f2ad1c0a95", + "note": "transactions#publishTransaction" + } + ], + [ + { + "op": "add", + "path": "/hash", + "value": "0x7ce19c0d128ca11293b44a4e6d3cc9063665c00ea8c8eb400f548e132c147353", + "note": "transactions#setTxHash" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "submitted", + "note": "txStateManager: setting status to submitted" + } + ], + [ + { + "op": "add", + "path": "/firstRetryBlockNumber", + "value": "0x478ab3", + "note": "transactions/pending-tx-tracker#event: tx:block-update" + } + ] + ], + "nonceDetails": { + "params": { + "highestLocalNonce": 53, + "highestSuggested": 53, + "nextNetworkNonce": 53 + }, + "local": { + "name": "local", + "nonce": 53, + "details": { + "startPoint": 53, + "highest": 53 + } + }, + "network": { + "name": "network", + "nonce": 53, + "details": { + "baseCount": 53 + } + } + }, + "rawTx": "0xf86c35850c1b710800827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0f5142ba79a13ca7ec65548953017edafb217803244bbf9821d9ad077d89921e9a03afcb614169c90be9905d5b469d06984825c76675d3a535937cdb8f2ad1c0a95", + "hash": "0x7ce19c0d128ca11293b44a4e6d3cc9063665c00ea8c8eb400f548e132c147353", + "firstRetryBlockNumber": "0x478ab3" + }, + { + "id": 3516145537630211, + "time": 1512613432658, + "status": "confirmed", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xba43b7400", + "gas": "0x7b0c", + "nonce": "0x34", + "chainId": "0x1" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208", + "history": [ + { + "id": 3516145537630211, + "time": 1512613432658, + "status": "unapproved", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xdf8475800", + "gas": "0x7b0c" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208" + }, + [ + { + "op": "replace", + "path": "/txParams/gasPrice", + "value": "0xba43b7400", + "note": "confTx: user approved transaction" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "approved", + "note": "txStateManager: setting status to approved" + } + ], + [ + { + "op": "add", + "path": "/txParams/nonce", + "value": "0x34", + "note": "transactions#approveTransaction" + }, + { + "op": "add", + "path": "/nonceDetails", + "value": { + "params": { + "highestLocalNonce": 52, + "highestSuggested": 52, + "nextNetworkNonce": 52 + }, + "local": { + "name": "local", + "nonce": 52, + "details": { + "startPoint": 52, + "highest": 52 + } + }, + "network": { + "name": "network", + "nonce": 52, + "details": { + "baseCount": 52 + } + } + } + } + ], + [ + { + "op": "add", + "path": "/txParams/chainId", + "value": "0x1", + "note": "txStateManager: setting status to signed" + }, + { + "op": "replace", + "path": "/status", + "value": "signed" + } + ], + [ + { + "op": "add", + "path": "/rawTx", + "value": "0xf86c34850ba43b7400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a073a4afdb8e8ad32b0cf9039af56c66baffd60d30e75cee5c1b783208824eafb8a0021ca6c1714a2c71281333ab77f776d3514348ab77967280fca8a5b4be44285e", + "note": "transactions#publishTransaction" + } + ], + [ + { + "op": "add", + "path": "/hash", + "value": "0x5c98409883fdfd3cd24058a83b91470da6c40ffae41a40eb90d7dee0b837d26d", + "note": "transactions#setTxHash" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "submitted", + "note": "txStateManager: setting status to submitted" + } + ], + [ + { + "op": "add", + "path": "/firstRetryBlockNumber", + "value": "0x478a2c", + "note": "transactions/pending-tx-tracker#event: tx:block-update" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "confirmed", + "note": "txStateManager: setting status to confirmed" + } + ] + ], + "nonceDetails": { + "params": { + "highestLocalNonce": 52, + "highestSuggested": 52, + "nextNetworkNonce": 52 + }, + "local": { + "name": "local", + "nonce": 52, + "details": { + "startPoint": 52, + "highest": 52 + } + }, + "network": { + "name": "network", + "nonce": 52, + "details": { + "baseCount": 52 + } + } + }, + "rawTx": "0xf86c34850ba43b7400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a073a4afdb8e8ad32b0cf9039af56c66baffd60d30e75cee5c1b783208824eafb8a0021ca6c1714a2c71281333ab77f776d3514348ab77967280fca8a5b4be44285e", + "hash": "0x5c98409883fdfd3cd24058a83b91470da6c40ffae41a40eb90d7dee0b837d26d", + "firstRetryBlockNumber": "0x478a2c" + }, + { + "id": 3516145537630210, + "time": 1512612826136, + "status": "confirmed", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xa7a358200", + "gas": "0x7b0c", + "nonce": "0x33", + "chainId": "0x1" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208", + "history": [ + { + "id": 3516145537630210, + "time": 1512612826136, + "status": "unapproved", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xba43b7400", + "gas": "0x7b0c" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208" + }, + [ + { + "op": "replace", + "path": "/txParams/gasPrice", + "value": "0xa7a358200", + "note": "confTx: user approved transaction" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "approved", + "note": "txStateManager: setting status to approved" + } + ], + [ + { + "op": "add", + "path": "/txParams/nonce", + "value": "0x33", + "note": "transactions#approveTransaction" + }, + { + "op": "add", + "path": "/nonceDetails", + "value": { + "params": { + "highestLocalNonce": 0, + "highestSuggested": 51, + "nextNetworkNonce": 51 + }, + "local": { + "name": "local", + "nonce": 51, + "details": { + "startPoint": 51, + "highest": 51 + } + }, + "network": { + "name": "network", + "nonce": 51, + "details": { + "baseCount": 51 + } + } + } + } + ], + [ + { + "op": "add", + "path": "/txParams/chainId", + "value": "0x1", + "note": "txStateManager: setting status to signed" + }, + { + "op": "replace", + "path": "/status", + "value": "signed" + } + ], + [ + { + "op": "add", + "path": "/rawTx", + "value": "0xf86c33850a7a358200827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0021a8cd6c10208cc593e22af53637e5d127cee5cc6f9443a3e758a02afff1d7ca025f7420e974d3f2c668c165040987c72543a8e709bfea3528a62836a6ced9ce8", + "note": "transactions#publishTransaction" + } + ], + [ + { + "op": "add", + "path": "/hash", + "value": "0x289772800898bc9cd414530d8581c0da257a9055e4aaaa6d10d92d700bfbd044", + "note": "transactions#setTxHash" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "submitted", + "note": "txStateManager: setting status to submitted" + } + ], + [ + { + "op": "add", + "path": "/firstRetryBlockNumber", + "value": "0x478a04", + "note": "transactions/pending-tx-tracker#event: tx:block-update" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "confirmed", + "note": "txStateManager: setting status to confirmed" + } + ] + ], + "nonceDetails": { + "params": { + "highestLocalNonce": 0, + "highestSuggested": 51, + "nextNetworkNonce": 51 + }, + "local": { + "name": "local", + "nonce": 51, + "details": { + "startPoint": 51, + "highest": 51 + } + }, + "network": { + "name": "network", + "nonce": 51, + "details": { + "baseCount": 51 + } + } + }, + "rawTx": "0xf86c33850a7a358200827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0021a8cd6c10208cc593e22af53637e5d127cee5cc6f9443a3e758a02afff1d7ca025f7420e974d3f2c668c165040987c72543a8e709bfea3528a62836a6ced9ce8", + "hash": "0x289772800898bc9cd414530d8581c0da257a9055e4aaaa6d10d92d700bfbd044", + "firstRetryBlockNumber": "0x478a04" + }, + { + "id": 3516145537630209, + "time": 1512612809252, + "status": "failed", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0x77359400", + "gas": "0x7b0c", + "nonce": "0x33", + "chainId": "0x1" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208", + "history": [ + { + "id": 3516145537630209, + "time": 1512612809252, + "status": "unapproved", + "metamaskNetworkId": "1", + "txParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "value": "0x16345785d8a0000", + "gasPrice": "0xba43b7400", + "gas": "0x7b0c" + }, + "gasPriceSpecified": false, + "gasLimitSpecified": false, + "estimatedGas": "5208" + }, + [ + { + "op": "replace", + "path": "/txParams/gasPrice", + "value": "0x77359400", + "note": "confTx: user approved transaction" + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "approved", + "note": "txStateManager: setting status to approved" + } + ], + [ + { + "op": "add", + "path": "/txParams/nonce", + "value": "0x33", + "note": "transactions#approveTransaction" + }, + { + "op": "add", + "path": "/nonceDetails", + "value": { + "params": { + "highestLocalNonce": 0, + "highestSuggested": 51, + "nextNetworkNonce": 51 + }, + "local": { + "name": "local", + "nonce": 51, + "details": { + "startPoint": 51, + "highest": 51 + } + }, + "network": { + "name": "network", + "nonce": 51, + "details": { + "baseCount": 51 + } + } + } + } + ], + [ + { + "op": "add", + "path": "/txParams/chainId", + "value": "0x1", + "note": "txStateManager: setting status to signed" + }, + { + "op": "replace", + "path": "/status", + "value": "signed" + } + ], + [ + { + "op": "add", + "path": "/rawTx", + "value": "0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7", + "note": "transactions#publishTransaction" + } + ], + [ + { + "op": "add", + "path": "/err", + "value": { + "message": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced", + "stack": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:60327:26\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88030:9\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16678:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16522:25)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16694:16\n at resultObj.id (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88012:9)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16813:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16527:17)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)" + } + } + ], + [ + { + "op": "replace", + "path": "/status", + "value": "failed", + "note": "txStateManager: setting status to failed" + } + ] + ], + "nonceDetails": { + "params": { + "highestLocalNonce": 0, + "highestSuggested": 51, + "nextNetworkNonce": 51 + }, + "local": { + "name": "local", + "nonce": 51, + "details": { + "startPoint": 51, + "highest": 51 + } + }, + "network": { + "name": "network", + "nonce": 51, + "details": { + "baseCount": 51 + } + } + }, + "rawTx": "0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7", + "err": { + "message": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced", + "stack": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:60327:26\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88030:9\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16678:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16522:25)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16694:16\n at resultObj.id (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88012:9)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16813:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16527:17)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)" + } + } + ], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "unapprovedTypedMessages": {}, + "unapprovedTypedMessagesCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + ] + } + ], + "computedBalances": {}, + "currentAccountTab": "history", + "tokens": [ + { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": "18" + } + ], + "selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "currentCurrency": "usd", + "conversionRate": 418.62, + "conversionDate": 1512615622, + "infuraNetworkStatus": { + "mainnet": "ok", + "ropsten": "ok", + "kovan": "ok", + "rinkeby": "ok" + }, + "shapeShiftTxList": [], + "lostAccounts": [] + }, + "appState": { + "shouldClose": true, + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "accountDetail": { + "subview": "transactions", + "accountExport": "none", + "privateKey": "" + }, + "transForward": false, + "isLoading": false, + "warning": null, + "forgottenPassword": false, + "scrollToBottom": false + }, + "identities": {}, + "version": "3.12.1", + "platform": { + "arch": "x86-64", + "nacl_arch": "x86-64", + "os": "mac" + }, + "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36" +} \ No newline at end of file diff --git a/development/states/send-edit.json b/development/states/send-edit.json new file mode 100644 index 000000000..6ea8e64cd --- /dev/null +++ b/development/states/send-edit.json @@ -0,0 +1,154 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "featureFlags": {"betaUI": true}, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Send Account 1" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "name": "Send Account 2" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "name": "Send Account 3" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "address": "0xd85a4b6a394794842887b8284293d69163007bbb", + "name": "Send Account 4" + } + }, + "unapprovedTxs": {}, + "currentCurrency": "USD", + "conversionRate": 1200.88200327, + "conversionDate": 1489013762, + "noActiveNotices": true, + "frequentRpcList": [], + "network": "3", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "code": "0x", + "balance": "0x47c9d71831c76efe", + "nonce": "0x1b", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "code": "0x", + "balance": "0x37452b1315889f80", + "nonce": "0xa", + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "code": "0x", + "balance": "0x30c9d71831c76efe", + "nonce": "0x1c", + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0xd85a4b6a394794842887b8284293d69163007bbb" + } + }, + "addressBook": [ + { + "address": "0x06195827297c7a80a443b6894d3bdb8824b43896", + "name": "Address Book Account 1" + } + ], + "tokens": [], + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedTxs": { + "4768706228115573": { + "id": 4768706228115573, + "time": 1487363153561, + "status": "unapproved", + "gasMultiplier": 1, + "metamaskNetworkId": "3", + "txParams": { + "from": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "to": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "value": "0x1bc16d674ec80000", + "metamaskId": 4768706228115573, + "metamaskNetworkId": "3", + "gas": "0xea60", + "gasPrice": "0xba43b7400" + } + } + }, + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + }, + { + "type": "Simple Key Pair", + "accounts": [ + "0xd85a4b6a394794842887b8284293d69163007bbb" + ] + } + ], + "selectedAddress": "0xd85a4b6a394794842887b8284293d69163007bbb", + "currentCurrency": "USD", + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "send": { + "gasLimit": "0xea60", + "gasPrice": "0xba43b7400", + "gasTotal": "0xb451dc41b578", + "tokenBalance": null, + "from": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "balance": "0x37452b1315889f80" + }, + "to": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "amount": "0x1bc16d674ec80000", + "memo": "", + "errors": {}, + "maxModeOn": false, + "editingTransactionId": null + } + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "confTx", + "detailView": null, + "context": 0 + }, + "accountDetail": { + "subview": "transactions" + }, + "modal": { + "modalState": {}, + "previousModalState": {} + }, + "transForward": true, + "isLoading": false, + "warning": null, + "scrollToBottom": false, + "forgottenPassword": null + }, + "identities": {} +} diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json new file mode 100644 index 000000000..a0a2c66e4 --- /dev/null +++ b/development/states/send-new-ui.json @@ -0,0 +1,133 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "featureFlags": {"betaUI": true}, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Send Account 1" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "name": "Send Account 2" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "name": "Send Account 3" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "address": "0xd85a4b6a394794842887b8284293d69163007bbb", + "name": "Send Account 4" + } + }, + "unapprovedTxs": {}, + "currentCurrency": "USD", + "conversionRate": 1200.88200327, + "conversionDate": 1489013762, + "noActiveNotices": true, + "frequentRpcList": [], + "network": "3", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "code": "0x", + "balance": "0x47c9d71831c76efe", + "nonce": "0x1b", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "code": "0x", + "balance": "0x37452b1315889f80", + "nonce": "0xa", + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "code": "0x", + "balance": "0x30c9d71831c76efe", + "nonce": "0x1c", + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + }, + "0xd85a4b6a394794842887b8284293d69163007bbb": { + "code": "0x", + "balance": "0x0", + "nonce": "0x0", + "address": "0xd85a4b6a394794842887b8284293d69163007bbb" + } + }, + "addressBook": [ + { + "address": "0x06195827297c7a80a443b6894d3bdb8824b43896", + "name": "Address Book Account 1" + } + ], + "tokens": [], + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + }, + { + "type": "Simple Key Pair", + "accounts": [ + "0xd85a4b6a394794842887b8284293d69163007bbb" + ] + } + ], + "selectedAddress": "0xd85a4b6a394794842887b8284293d69163007bbb", + "currentCurrency": "USD", + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [], + "send": { + "gasLimit": null, + "gasPrice": null, + "gasTotal": "0xb451dc41b578", + "tokenBalance": null, + "from": "", + "to": "", + "amount": "0x0", + "memo": "", + "errors": {}, + "maxModeOn": false, + "editingTransactionId": null + } + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "accountDetail", + "detailView": null, + "context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc" + }, + "accountDetail": { + "subview": "transactions" + }, + "modal": { + "modalState": {}, + "previousModalState": {} + }, + "transForward": true, + "isLoading": false, + "warning": null, + "scrollToBottom": false, + "forgottenPassword": null + }, + "identities": {} +} diff --git a/development/test.html b/development/test.html index 702be7fa0..49084c0a4 100644 --- a/development/test.html +++ b/development/test.html @@ -18,13 +18,14 @@ diff --git a/development/uiStore.js b/development/uiStore.js index 1299ee1dc..c71d66d3b 100644 --- a/development/uiStore.js +++ b/development/uiStore.js @@ -1,7 +1,7 @@ const createStore = require('redux').createStore const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const createLogger = require('redux-logger') +const thunkMiddleware = require('redux-thunk').default +const createLogger = require('redux-logger').createLogger const rootReducer = require('../ui/app/reducers') module.exports = configureStore diff --git a/docker-compose.yml b/docker-compose.yml index 58c046c32..9a57617dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ metamascara: ports: - "9001" environment: - MASCARA_ORIGIN: "https://zero.metamask.io" + MASCARA_ORIGIN: "https://wallet.metamask.io" VIRTUAL_PORT: "9001" - VIRTUAL_HOST: "zero.metamask.io" - LETSENCRYPT_HOST: "zero.metamask.io" + VIRTUAL_HOST: "wallet.metamask.io" + LETSENCRYPT_HOST: "wallet.metamask.io" LETSENCRYPT_EMAIL: "admin@metamask.io" \ No newline at end of file diff --git a/docs/add-to-chrome.md b/docs/add-to-chrome.md new file mode 100644 index 000000000..ea5213182 --- /dev/null +++ b/docs/add-to-chrome.md @@ -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. + diff --git a/docs/add-to-firefox.md b/docs/add-to-firefox.md new file mode 100644 index 000000000..593d06170 --- /dev/null +++ b/docs/add-to-firefox.md @@ -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`. + diff --git a/docs/adding-new-networks.md b/docs/adding-new-networks.md new file mode 100644 index 000000000..ea1453c21 --- /dev/null +++ b/docs/adding-new-networks.md @@ -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 + diff --git a/docs/developing-on-deps.md b/docs/developing-on-deps.md new file mode 100644 index 000000000..7de3f67a8 --- /dev/null +++ b/docs/developing-on-deps.md @@ -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! + diff --git a/docs/development-visualization.md b/docs/development-visualization.md new file mode 100644 index 000000000..95847300d --- /dev/null +++ b/docs/development-visualization.md @@ -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 +``` + diff --git a/docs/notices.md b/docs/notices.md new file mode 100644 index 000000000..826e6e84e --- /dev/null +++ b/docs/notices.md @@ -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. + diff --git a/docs/porting_to_new_environment.md b/docs/porting_to_new_environment.md new file mode 100644 index 000000000..729a28e5d --- /dev/null +++ b/docs/porting_to_new_environment.md @@ -0,0 +1,92 @@ +# Guide to Porting MetaMask to a New Environment + +MetaMask has been under continuous development for nearly two years now, and we’ve gradually discovered some very useful abstractions, that have allowed us to grow more easily. A couple of those layers together allow MetaMask to be ported to new environments and contexts increasingly easily. + +### The MetaMask Controller + +The core functionality of MetaMask all lives in what we call [The MetaMask Controller](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js). Our goal for this file is for it to eventually be its own javascript module that can be imported into any JS-compatible context, allowing it to fully manage an app's relationship to Ethereum. + +#### Constructor + +When calling `new MetaMask(opts)`, many platform-specific options are configured. The keys on `opts` are as follows: + +- initState: The last emitted state, used for restoring persistent state between sessions. +- platform: The `platform` object defines a variety of platform-specific functions, including opening the confirmation view, and opening web sites. +- encryptor - An object that provides access to the desired encryption methods. + +##### Encryptor + +An object that provides two simple methods, which can encrypt in any format you prefer. This parameter is optional, and will default to the browser-native WebCrypto API. + +- encrypt(password, object) - returns a Promise of a string that is ready for storage. +- decrypt(password, encryptedString) - Accepts the encrypted output of `encrypt` and returns a Promise of a restored `object` as it was encrypted. + + +##### Platform Options + +The `platform` object has a variety of options: + +- reload (function) - Will be called when MetaMask would like to reload its own context. +- openWindow ({ url }) - Will be called when MetaMask would like to open a web page. It will be passed a single `options` object with a `url` key, with a string value. +- getVersion() - Should return the current MetaMask version, as described in the current `CHANGELOG.md` or `app/manifest.json`. + +#### [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) + +This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated) + +#### [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335) + +Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works! + +### The UI + +The MetaMask UI is essentially just a website that can be configured by passing it the API and state subscriptions from above. Anyone could make a UI that consumes these, effectively reskinning MetaMask. + +You can see this in action in our file [ui/index.js](https://github.com/MetaMask/metamask-extension/blob/master/ui/index.js). There you can see an argument being passed in named `accountManager`, which is essentially a MetaMask controller (forgive its really outdated parameter name!). With access to that object, the UI is able to initialize a whole React/Redux app that relies on this API for its account/blockchain-related/persistent states. + +## Putting it Together + +As an example, a WebExtension is always defined by a `manifest.json` file. [In ours](https://github.com/MetaMask/metamask-extension/blob/master/app/manifest.json#L31), you can see that [background.js](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/background.js) is defined as a script to run in the background, and this is the file that we use to initialize the MetaMask controller. + +In that file, there's a lot going on, so it's maybe worth focusing on our MetaMask controller constructor to start. It looks something like this: + +```javascript +const controller = new MetamaskController({ + // User confirmation callbacks: + showUnconfirmedMessage: triggerUi, + unlockAccountMessage: triggerUi, + showUnapprovedTx: triggerUi, + // initial state + initState, + // platform specific api + platform, +}) +``` +Since `background.js` is essentially the Extension setup file, we can see it doing all the things specific to the extension platform: +- Defining how to open the UI for new messages, transactions, and even requests to unlock (reveal to the site) their account. +- Provide the instance's initial state, leaving MetaMask persistence to the platform. +- Providing a `platform` object. This is becoming our catch-all adapter for platforms to define a few other platform-variant features we require, like opening a web link. (Soon we will be moving encryption out here too, since our browser-encryption isn't portable enough!) + +## Ports, streams, and Web3! + +Everything so far has been enough to create a MetaMask wallet on virtually any platform that runs JS, but MetaMask's most unique feature isn't being a wallet, it's providing an Ethereum-enabled JavaScript context to websites. + +MetaMask has two kinds of [duplex stream APIs](https://github.com/substack/stream-handbook#duplex) that it exposes: +- [metamask.setupTrustedCommunication(connectionStream, originDomain)](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L352) - This stream is used to connect the user interface over a remote port, and may not be necessary for contexts where the interface and the metamask-controller share a process. +- [metamask.setupUntrustedCommunication(connectionStream, originDomain)](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L337) - This method is used to connect a new web site's web3 API to MetaMask's blockchain connection. Additionally, the `originDomain` is used to block detected phishing sites. + +### Web3 as a Stream + +If you are making a MetaMask-powered browser for a new platform, one of the trickiest tasks will be injecting the Web3 API into websites that are visited. On WebExtensions, we actually have to pipe data through a total of three JS contexts just to let sites talk to our background process (site -> contentscript -> background). + +To make this as easy as possible, we use one of our favorite internal tools, [web3-provider-engine](https://www.npmjs.com/package/web3-provider-engine) to construct a custom web3 provider object whose source of truth is a stream that we connect to remotely. + +To see how we do that, you can refer to the [inpage script](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/inpage.js) that we inject into every website. There you can see it creates a multiplex stream to the background, and uses it to initialize what we call the [inpage-provider](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/lib/inpage-provider.js), which you can see stubs a few methods out, but mostly just passes calls to `sendAsync` through the stream it's passed! That's really all the magic that's needed to create a web3-like API in a remote context, once you have a stream to MetaMask available. + +In `inpage.js` you can see we create a `PortStream`, that's just a class we use to wrap WebExtension ports as streams, so we can reuse our favorite stream abstraction over the more irregular API surface of the WebExtension. In a new platform, you will probably need to construct this stream differently. The key is that you need to construct a stream that talks from the site context to the background. Once you have that set up, it works like magic! + +If streams seem new and confusing to you, that's ok, they can seem strange at first. To help learn them, we highly recommend reading Substack's [Stream Handbook](https://github.com/substack/stream-handbook), or going through NodeSchool's interactive command-line class [Stream Adventure](https://github.com/workshopper/stream-adventure), also maintained by Substack. + +## Conclusion + +I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)! diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 000000000..00369acf9 --- /dev/null +++ b/docs/publishing.md @@ -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. + diff --git a/docs/ui-dev-mode.md b/docs/ui-dev-mode.md new file mode 100644 index 000000000..df49d8b04 --- /dev/null +++ b/docs/ui-dev-mode.md @@ -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. + diff --git a/docs/ui-mock-mode.md b/docs/ui-mock-mode.md new file mode 100644 index 000000000..bb54dc471 --- /dev/null +++ b/docs/ui-mock-mode.md @@ -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. + diff --git a/gulpfile.js b/gulpfile.js index fe223adf1..3ade82f87 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -19,10 +19,20 @@ var manifest = require('./app/manifest.json') var gulpif = require('gulp-if') var replace = require('gulp-replace') var mkdirp = require('mkdirp') +var asyncEach = require('async/each') +var exec = require('child_process').exec +var sass = require('gulp-sass') +var autoprefixer = require('gulp-autoprefixer') +var gulpStylelint = require('gulp-stylelint') +var stylefmt = require('gulp-stylefmt') +var uglify = require('gulp-uglify-es').default +var babel = require('gulp-babel') -var disableLiveReload = gutil.env.disableLiveReload + +var disableDebugTools = gutil.env.disableDebugTools var debug = gutil.env.debug + // browser reload gulp.task('dev:reload', function() { @@ -52,6 +62,15 @@ gulp.task('copy:images', copyTask({ './dist/opera/images', ], })) +gulp.task('copy:contractImages', copyTask({ + source: './node_modules/eth-contract-metadata/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: [ @@ -111,11 +130,17 @@ gulp.task('manifest:production', function() { './dist/firefox/manifest.json', './dist/chrome/manifest.json', './dist/edge/manifest.json', + './dist/opera/manifest.json', ],{base: './dist/'}) - .pipe(gulpif(disableLiveReload,jsoneditor(function(json) { - json.background.scripts = ["scripts/background.js"] + + // Exclude chromereload script in production: + .pipe(gulpif(!debug,jsoneditor(function(json) { + json.background.scripts = json.background.scripts.filter((script) => { + return !script.includes('chromereload') + }) return json }))) + .pipe(gulp.dest('./dist/', { overwrite: true })) }) @@ -127,8 +152,9 @@ const staticFiles = [ ] var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`) +copyStrings.push('copy:contractImages') -if (!disableLiveReload) { +if (debug) { copyStrings.push('copy:reload') } @@ -137,11 +163,23 @@ gulp.task('copy:watch', function(){ gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy')) }) +// record deps + +gulp.task('deps', function (cb) { + exec('npm ls', (err, stdoutOutput, stderrOutput) => { + if (err) return cb(err) + const browsers = ['firefox','chrome','edge','opera'] + asyncEach(browsers, (target, done) => { + fs.writeFile(`./dist/${target}/deps.txt`, stdoutOutput, done) + }, cb) + }) +}) + // lint js gulp.task('lint', function () { // Ignoring node_modules, dist/firefox, and docs folders: - return gulp.src(['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js']) + return gulp.src(['app/**/*.js', '!app/scripts/vendor/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']) .pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc')))) // eslint.format() outputs the lint results to the console. // Alternatively use eslint.formatEach() (see Docs). @@ -151,6 +189,13 @@ gulp.task('lint', function () { .pipe(eslint.failAfterError()) }); +gulp.task('lint:fix', function () { + return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js']) + .pipe(eslint(Object.assign(fs.readFileSync(path.join(__dirname, '.eslintrc')), {fix: true}))) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()) +}); + /* gulp.task('default', ['lint'], function () { // This will only run if the lint task is successful... @@ -166,23 +211,69 @@ const jsFiles = [ 'popup', ] +// scss compilation and autoprefixing tasks + +gulp.task('build:scss', function () { + return gulp.src('ui/app/css/index.scss') + .pipe(sourcemaps.init()) + .pipe(sass().on('error', sass.logError)) + .pipe(sourcemaps.write()) + .pipe(autoprefixer()) + .pipe(gulp.dest('ui/app/css/output')) +}) +gulp.task('watch:scss', function() { + gulp.watch(['ui/app/css/**/*.scss'], gulp.series(['build:scss'])) +}) + +gulp.task('lint-scss', function() { + return gulp + .src('ui/app/css/itcss/**/*.scss') + .pipe(gulpStylelint({ + reporters: [ + {formatter: 'string', console: true} + ], + fix: true, + })); +}); + +gulp.task('fmt-scss', function () { + return gulp.src('ui/app/css/itcss/**/*.scss') + .pipe(stylefmt()) + .pipe(gulp.dest('ui/app/css/itcss')); +}); + // bundle tasks var jsDevStrings = jsFiles.map(jsFile => `dev:js:${jsFile}`) var jsBuildStrings = jsFiles.map(jsFile => `build:js:${jsFile}`) jsFiles.forEach((jsFile) => { - gulp.task(`dev:js:${jsFile}`, bundleTask({ watch: true, label: jsFile, filename: `${jsFile}.js` })) - gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, label: jsFile, filename: `${jsFile}.js` })) + gulp.task(`dev:js:${jsFile}`, bundleTask({ + watch: true, + label: jsFile, + filename: `${jsFile}.js`, + isBuild: false + })) + gulp.task(`build:js:${jsFile}`, bundleTask({ + watch: false, + label: jsFile, + filename: `${jsFile}.js`, + isBuild: true + })) }) -gulp.task('dev:js', gulp.parallel(...jsDevStrings)) -gulp.task('build:js', gulp.parallel(...jsBuildStrings)) +// inpage must be built before all other scripts: +const firstDevString = jsDevStrings.shift() +gulp.task('dev:js', gulp.series(firstDevString, gulp.parallel(...jsDevStrings))) + +// inpage must be built before all other scripts: +const firstBuildString = jsBuildStrings.shift() +gulp.task('build:js', gulp.series(firstBuildString, 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}`))) @@ -202,12 +293,18 @@ gulp.task('zip:edge', zipTask('edge')) gulp.task('zip:opera', zipTask('opera')) gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge', 'zip:opera')) +// set env var for production +gulp.task('apply-prod-environment', function(done) { + process.env.NODE_ENV = 'production' + done() +}); + // high level tasks -gulp.task('dev', gulp.series('dev:js', 'copy', gulp.parallel('copy:watch', 'dev:reload'))) +gulp.task('dev', gulp.series('build:scss', 'dev:js', 'copy', gulp.parallel('watch:scss', 'copy:watch', 'dev:reload'))) -gulp.task('build', gulp.series('clean', gulp.parallel('build:js', 'copy'))) -gulp.task('dist', gulp.series('build', 'zip')) +gulp.task('build', gulp.series('clean', 'build:scss', gulp.parallel('build:js', 'copy'))) +gulp.task('dist', gulp.series('apply-prod-environment', 'build', 'zip')) // task generators @@ -224,7 +321,7 @@ function copyTask(opts){ destinations.forEach(function(destination) { stream = stream.pipe(gulp.dest(destination)) }) - stream.pipe(gulpif(!disableLiveReload,livereload())) + stream.pipe(gulpif(debug,livereload())) return stream } @@ -234,30 +331,31 @@ function zipTask(target) { return () => { return gulp.src(`dist/${target}/**`) .pipe(zip(`metamask-${target}-${manifest.version}.zip`)) - .pipe(gulp.dest('builds')); + .pipe(gulp.dest('builds')) } } -function generateBundler(opts) { - var browserifyOpts = assign({}, watchify.args, { +function generateBundler(opts, performBundle) { + const browserifyOpts = assign({}, watchify.args, { entries: ['./app/scripts/'+opts.filename], plugin: 'browserify-derequire', debug: debug, fullPaths: debug, }) - return browserify(browserifyOpts) -} - -function discTask(opts) { - let bundler = generateBundler(opts) + let bundler = browserify(browserifyOpts) if (opts.watch) { bundler = watchify(bundler) - // on any dep update, runs the bundler + // on any file update, re-runs the bundler bundler.on('update', performBundle) } + return bundler +} + +function discTask(opts) { + const bundler = generateBundler(opts, performBundle) // output build logs to terminal bundler.on('log', gutil.log) @@ -279,14 +377,7 @@ function discTask(opts) { function bundleTask(opts) { - let bundler = generateBundler(opts) - - if (opts.watch) { - bundler = watchify(bundler) - // on any file update, re-runs the bundler - bundler.on('update', performBundle) - } - + const bundler = generateBundler(opts, performBundle) // output build logs to terminal bundler.on('log', gutil.log) @@ -296,8 +387,16 @@ function bundleTask(opts) { return ( bundler.bundle() - // log errors if they happen - .on('error', gutil.log.bind(gutil, 'Browserify Error')) + + // handle errors + .on('error', (err) => { + beep() + if (opts.watch) { + console.warn(err.stack) + } else { + throw err + } + }) // convert bundle stream to gulp vinyl stream .pipe(source(opts.filename)) // inject variables into bundle @@ -306,17 +405,23 @@ function bundleTask(opts) { .pipe(buffer()) // sourcemaps // loads map from browserify file - .pipe(sourcemaps.init({loadMaps: true})) + .pipe(gulpif(debug, sourcemaps.init({ loadMaps: true }))) + // Minification + .pipe(gulpif(opts.isBuild, uglify())) // writes .map file - .pipe(sourcemaps.write('./')) + .pipe(gulpif(debug, sourcemaps.write('./'))) // write completed bundles .pipe(gulp.dest('./dist/firefox/scripts')) .pipe(gulp.dest('./dist/chrome/scripts')) .pipe(gulp.dest('./dist/edge/scripts')) .pipe(gulp.dest('./dist/opera/scripts')) // finally, trigger live reload - .pipe(gulpif(!disableLiveReload, livereload())) + .pipe(gulpif(debug, livereload())) ) } } + +function beep () { + process.stdout.write('\x07') +} diff --git a/mascara/README.md b/mascara/README.md index d79f04ae2..6e3bfe96b 100644 --- a/mascara/README.md +++ b/mascara/README.md @@ -1,26 +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 - - - [ ] Figure out user flows and UI redesign - - [ ] Figure out FireFox - Standing problems: - - [ ] IndexDb - - -### deploy +### Tests: ``` -docker-compose build && docker-compose stop && docker-compose up -d && docker-compose logs -f --tail 10 +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 ``` \ No newline at end of file diff --git a/mascara/example/app.js b/mascara/example/app.js index aae7ccd19..598e2c84c 100644 --- a/mascara/example/app.js +++ b/mascara/example/app.js @@ -1,57 +1,38 @@ -window.addEventListener('load', web3Detect) +const EthQuery = require('ethjs-query') + +window.addEventListener('load', loadProvider) window.addEventListener('message', console.warn) -function web3Detect() { - if (global.web3) { - logToDom('web3 detected!') - startApp() - } else { - logToDom('no web3 detected!') - } +async function loadProvider() { + const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' }) + const ethQuery = new EthQuery(ethereumProvider) + const accounts = await ethQuery.accounts() + window.METAMASK_ACCOUNT = accounts[0] || 'locked' + logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account') + setupButtons(ethQuery) } -function startApp(){ - console.log('app started') - var primaryAccount - console.log('getting main account...') - web3.eth.getAccounts((err, addresses) => { - if (err) console.error(err) - console.log('set address', addresses[0]) - primaryAccount = addresses[0] - }) - - document.querySelector('.action-button-1').addEventListener('click', function(){ - console.log('saw click') - console.log('sending tx') - primaryAccount - web3.eth.sendTransaction({ - from: primaryAccount, - to: primaryAccount, - value: 0, - }, function(err, txHash){ - if (err) throw err - console.log('sendTransaction result:', err || txHash) - }) - }) - document.querySelector('.action-button-2').addEventListener('click', function(){ - console.log('saw click') - setTimeout(function(){ - console.log('sending tx') - web3.eth.sendTransaction({ - from: primaryAccount, - to: primaryAccount, - value: 0, - }, function(err, txHash){ - if (err) throw err - console.log('sendTransaction result:', err || txHash) - }) - }) - }) - -} - -function logToDom(message){ - document.body.appendChild(document.createTextNode(message)) +function logToDom(message, context){ + document.getElementById(context).innerText = message console.log(message) } + +function setupButtons (ethQuery) { + const accountButton = document.getElementById('action-button-1') + accountButton.addEventListener('click', async () => { + const accounts = await ethQuery.accounts() + window.METAMASK_ACCOUNT = accounts[0] || 'locked' + logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account') + }) + const txButton = document.getElementById('action-button-2') + txButton.addEventListener('click', async () => { + if (!window.METAMASK_ACCOUNT || window.METAMASK_ACCOUNT === 'locked') return + const txHash = await ethQuery.sendTransaction({ + from: window.METAMASK_ACCOUNT, + to: window.METAMASK_ACCOUNT, + data: '', + }) + logToDom(txHash, 'cb-value') + }) +} \ No newline at end of file diff --git a/mascara/example/app/index.html b/mascara/example/app/index.html index 02323e5f9..8afb6f3f2 100644 --- a/mascara/example/app/index.html +++ b/mascara/example/app/index.html @@ -3,13 +3,15 @@ - MetaMask ZeroClient Example + MetaMask ZeroClient Example - - + +
+ +
\ No newline at end of file diff --git a/mascara/server/index.js b/mascara/server/index.js index 14e3fa18e..6fb1287cc 100644 --- a/mascara/server/index.js +++ b/mascara/server/index.js @@ -1,29 +1,33 @@ +const path = require('path') const express = require('express') const createBundle = require('./util').createBundle const serveBundle = require('./util').serveBundle +const compression = require('compression') module.exports = createMetamascaraServer -function createMetamascaraServer(){ +function createMetamascaraServer () { // start bundlers - const metamascaraBundle = createBundle(__dirname + '/../src/mascara.js') - const proxyBundle = createBundle(__dirname + '/../src/proxy.js') - const uiBundle = createBundle(__dirname + '/../src/ui.js') - const backgroundBuild = createBundle(__dirname + '/../src/background.js') + const metamascaraBundle = createBundle(path.join(__dirname, '/../src/mascara.js')) + const proxyBundle = createBundle(path.join(__dirname, '/../src/proxy.js')) + const uiBundle = createBundle(path.join(__dirname, '/../src/ui.js')) + const backgroundBuild = createBundle(path.join(__dirname, '/../src/background.js')) // serve bundles const server = express() + server.use(compression()) + // ui window serveBundle(server, '/ui.js', uiBundle) - server.use(express.static(__dirname+'/../ui/')) - server.use(express.static(__dirname+'/../../dist/chrome')) + server.use(express.static(path.join(__dirname, '/../ui/'), { setHeaders: (res) => res.set('X-Frame-Options', 'DENY') })) + server.use(express.static(path.join(__dirname, '/../../dist/chrome'))) // metamascara serveBundle(server, '/metamascara.js', metamascaraBundle) // proxy serveBundle(server, '/proxy/proxy.js', proxyBundle) - server.use('/proxy/', express.static(__dirname+'/../proxy')) + server.use('/proxy/', express.static(path.join(__dirname, '/../proxy'))) // background serveBundle(server, '/background.js', backgroundBuild) diff --git a/mascara/server/util.js b/mascara/server/util.js index 6e25b35d8..f9692afb6 100644 --- a/mascara/server/util.js +++ b/mascara/server/util.js @@ -7,14 +7,14 @@ module.exports = { } -function serveBundle(server, path, bundle){ - server.get(path, function(req, res){ +function serveBundle (server, path, bundle) { + server.get(path, function (req, res) { res.setHeader('Content-Type', 'application/javascript; charset=UTF-8') res.send(bundle.latest) }) } -function createBundle(entryPoint){ +function createBundle (entryPoint) { var bundleContainer = {} @@ -24,14 +24,16 @@ function createBundle(entryPoint){ packageCache: {}, plugin: [watchify], }) + .transform('babelify') + .transform('uglifyify', { global: true }) bundler.on('update', bundle) bundle() return bundleContainer - function bundle() { - bundler.bundle(function(err, result){ + function bundle () { + bundler.bundle(function (err, result) { if (err) { console.log(`Bundle failed! (${entryPoint})`) console.error(err) diff --git a/mascara/src/app/buy-ether-widget/index.js b/mascara/src/app/buy-ether-widget/index.js new file mode 100644 index 000000000..c60221a0a --- /dev/null +++ b/mascara/src/app/buy-ether-widget/index.js @@ -0,0 +1,198 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import {connect} from 'react-redux' +import {qrcode} from 'qrcode-npm' +import copyToClipboard from 'copy-to-clipboard' +import ShapeShiftForm from '../shapeshift-form' +import {buyEth, showAccountDetail} from '../../../../ui/app/actions' + +const OPTION_VALUES = { + COINBASE: 'coinbase', + SHAPESHIFT: 'shapeshift', + QR_CODE: 'qr_code', +} + +const OPTIONS = [ + { + name: 'Direct Deposit', + value: OPTION_VALUES.QR_CODE, + }, + { + name: 'Buy with Dollars', + value: OPTION_VALUES.COINBASE, + }, + { + name: 'Buy with Cryptos', + value: OPTION_VALUES.SHAPESHIFT, + }, +] + +class BuyEtherWidget extends Component { + + static propTypes = { + address: PropTypes.string, + skipText: PropTypes.string, + className: PropTypes.string, + onSkip: PropTypes.func, + goToCoinbase: PropTypes.func, + showAccountDetail: PropTypes.func, + }; + + state = { + selectedOption: OPTION_VALUES.QR_CODE, + }; + + + copyToClipboard = () => { + const { address } = this.props + + this.setState({ justCopied: true }, () => copyToClipboard(address)) + + setTimeout(() => this.setState({ justCopied: false }), 1000) + } + + renderSkip () { + const {showAccountDetail, address, skipText, onSkip} = this.props + + return ( +
{ + if (onSkip) return onSkip() + showAccountDetail(address) + }} + > + {skipText || 'Do it later'} +
+ ) + } + + renderCoinbaseLogo () { + return ( + + + + + + + + + + + + + + + ) + } + + renderCoinbaseForm () { + const {goToCoinbase, address} = this.props + + return ( +
+
{this.renderCoinbaseLogo()}
+
Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin.
+ What is Ethereum? +
+ +
+
+ ) + } + + renderContent () { + const { address } = this.props + const { justCopied } = this.state + const qrImage = qrcode(4, 'M') + qrImage.addData(address) + qrImage.make() + + switch (this.state.selectedOption) { + case OPTION_VALUES.COINBASE: + return this.renderCoinbaseForm() + case OPTION_VALUES.SHAPESHIFT: + return ( +
+
+
+ Trade any leading blockchain asset for any other. Protection by Design. No Account Needed. +
+ +
+ ) + case OPTION_VALUES.QR_CODE: + return ( +
+
+
Deposit Ether directly into your account.
+
(This is the account address that MetaMask created for you to recieve funds.)
+
+ +
+
+ ) + default: + return null + } + } + + render () { + const { className = '' } = this.props + const { selectedOption } = this.state + + return ( +
+
+
Deposit Options
+ {this.renderSkip()} +
+
+
+ {OPTIONS.map(({ name, value }) => ( +
this.setState({ selectedOption: value })} + > +
{name}
+ {value === selectedOption && ( + + + + )} +
+ ))} +
+
+ {this.renderContent()} +
+
+
+ ) + } +} + +export default connect( + ({ metamask: { selectedAddress } }) => ({ + address: selectedAddress, + }), + dispatch => ({ + goToCoinbase: address => dispatch(buyEth({ network: '1', address, amount: 0 })), + showAccountDetail: address => dispatch(showAccountDetail(address)), + }) +)(BuyEtherWidget) diff --git a/mascara/src/app/first-time/backup-phrase-screen.js b/mascara/src/app/first-time/backup-phrase-screen.js new file mode 100644 index 000000000..9db61f3ab --- /dev/null +++ b/mascara/src/app/first-time/backup-phrase-screen.js @@ -0,0 +1,255 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import classnames from 'classnames' +import shuffle from 'lodash.shuffle' +import {compose, onlyUpdateForPropTypes} from 'recompose' +import Identicon from '../../../../ui/app/components/identicon' +import {confirmSeedWords} from '../../../../ui/app/actions' +import Breadcrumbs from './breadcrumbs' +import LoadingScreen from './loading-screen' + +const LockIcon = props => ( + + + + + +); + +class BackupPhraseScreen extends Component { + static propTypes = { + isLoading: PropTypes.bool.isRequired, + address: PropTypes.string.isRequired, + seedWords: PropTypes.string.isRequired, + next: PropTypes.func.isRequired, + confirmSeedWords: PropTypes.func.isRequired, + }; + + static defaultProps = { + seedWords: '' + }; + + static PAGE = { + SECRET: 'secret', + CONFIRM: 'confirm' + }; + + constructor(props) { + const {seedWords} = props + super(props) + this.state = { + isShowingSecret: false, + page: BackupPhraseScreen.PAGE.SECRET, + selectedSeeds: [], + shuffledSeeds: seedWords && shuffle(seedWords.split(' ')), + } + } + + renderSecretWordsContainer () { + const { isShowingSecret } = this.state + + return ( +
+
+ {this.props.seedWords} +
+ {!isShowingSecret && ( +
+ + +
+ )} +
+ ); + } + + renderSecretScreen() { + const { isShowingSecret } = this.state + + return ( +
+
+
Secret Backup Phrase
+
+ Your secret backup phrase makes it easy to back up and restore your account. +
+
+ WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever. +
+ {this.renderSecretWordsContainer()} + + +
+
+
Tips:
+
+ Store this phrase in a password manager like 1password. +
+
+ Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations. +
+
+ Memorize this phrase. +
+
+
+ ) + } + + renderConfirmationScreen() { + const { seedWords, confirmSeedWords, next } = this.props; + const { selectedSeeds, shuffledSeeds } = this.state; + const isValid = seedWords === selectedSeeds.map(([_, seed]) => seed).join(' ') + + return ( +
+
+
Confirm your Secret Backup Phrase
+
+ Please select each phrase in order to make sure it is correct. +
+
+ {selectedSeeds.map(([_, word], i) => ( + + ))} +
+
+ {shuffledSeeds.map((word, i) => { + const isSelected = selectedSeeds + .filter(([index, seed]) => seed === word && index === i) + .length + + return ( + + ) + })} +
+ +
+
+ ) + } + + renderBack () { + return this.state.page === BackupPhraseScreen.PAGE.CONFIRM + ? ( + { + e.preventDefault() + this.setState({ + page: BackupPhraseScreen.PAGE.SECRET + }) + }} + href="#" + > + {`< Back`} + + ) + : null + } + + renderContent () { + switch (this.state.page) { + case BackupPhraseScreen.PAGE.CONFIRM: + return this.renderConfirmationScreen() + case BackupPhraseScreen.PAGE.SECRET: + default: + return this.renderSecretScreen() + } + } + + render () { + return this.props.isLoading + ? + : ( +
+ {this.renderBack()} + + {this.renderContent()} +
+ ) + } +} + +export default compose( + onlyUpdateForPropTypes, + connect( + ({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({ + seedWords, + isLoading, + address: selectedAddress, + }), + dispatch => ({ + confirmSeedWords: () => dispatch(confirmSeedWords()), + }) + ) +)(BackupPhraseScreen) diff --git a/mascara/src/app/first-time/breadcrumbs.js b/mascara/src/app/first-time/breadcrumbs.js new file mode 100644 index 000000000..b81a9fb9b --- /dev/null +++ b/mascara/src/app/first-time/breadcrumbs.js @@ -0,0 +1,26 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class Breadcrumbs extends Component { + + static propTypes = { + total: PropTypes.number, + currentIndex: PropTypes.number, + }; + + render() { + const {total, currentIndex} = this.props + return ( +
+ {Array(total).fill().map((_, i) => ( +
+ ))} +
+ ); + } + +} diff --git a/mascara/src/app/first-time/buy-ether-screen.js b/mascara/src/app/first-time/buy-ether-screen.js new file mode 100644 index 000000000..c5a560638 --- /dev/null +++ b/mascara/src/app/first-time/buy-ether-screen.js @@ -0,0 +1,200 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import {connect} from 'react-redux' +import {qrcode} from 'qrcode-npm' +import copyToClipboard from 'copy-to-clipboard' +import ShapeShiftForm from '../shapeshift-form' +import Identicon from '../../../../ui/app/components/identicon' +import {buyEth, showAccountDetail} from '../../../../ui/app/actions' + +class BuyEtherScreen extends Component { + static OPTION_VALUES = { + COINBASE: 'coinbase', + SHAPESHIFT: 'shapeshift', + QR_CODE: 'qr_code', + }; + + static OPTIONS = [ + { + name: 'Direct Deposit', + value: BuyEtherScreen.OPTION_VALUES.QR_CODE, + }, + { + name: 'Buy with Dollars', + value: BuyEtherScreen.OPTION_VALUES.COINBASE, + }, + { + name: 'Buy with Cryptos', + value: BuyEtherScreen.OPTION_VALUES.SHAPESHIFT, + }, + ]; + + static propTypes = { + address: PropTypes.string, + goToCoinbase: PropTypes.func.isRequired, + showAccountDetail: PropTypes.func.isRequired, + } + + state = { + selectedOption: BuyEtherScreen.OPTION_VALUES.QR_CODE, + justCopied: false, + } + + copyToClipboard = () => { + const { address } = this.props + + this.setState({ justCopied: true }, () => copyToClipboard(address)) + + setTimeout(() => this.setState({ justCopied: false }), 1000) + } + + renderSkip () { + const {showAccountDetail, address} = this.props + + return ( +
showAccountDetail(address)} + > + Do it later +
+ ) + } + + renderCoinbaseLogo () { + return ( + + + + + + + + + + + + + + + ) + } + + renderCoinbaseForm () { + const {goToCoinbase, address} = this.props + + return ( +
+
{this.renderCoinbaseLogo()}
+
Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin.
+ What is Ethereum? +
+ +
+
+ ) + } + + renderContent () { + const { OPTION_VALUES } = BuyEtherScreen + const { address } = this.props + const { justCopied } = this.state + const qrImage = qrcode(4, 'M') + qrImage.addData(address) + qrImage.make() + + switch (this.state.selectedOption) { + case OPTION_VALUES.COINBASE: + return this.renderCoinbaseForm() + case OPTION_VALUES.SHAPESHIFT: + return ( +
+
+
+ Trade any leading blockchain asset for any other. Protection by Design. No Account Needed. +
+ +
+ ) + case OPTION_VALUES.QR_CODE: + return ( +
+
+
Deposit Ether directly into your account.
+
(This is the account address that MetaMask created for you to recieve funds.)
+
+ +
+
+ ) + default: + return null + } + } + + render () { + const { OPTIONS } = BuyEtherScreen + const { selectedOption } = this.state + + return ( +
+ +
Deposit Ether
+
+ MetaMask works best if you have Ether in your account to pay for transaction gas fees and more. To get Ether, choose from one of these methods. +
+
+
+
Deposit Options
+ {this.renderSkip()} +
+
+
+ {OPTIONS.map(({ name, value }) => ( +
this.setState({ selectedOption: value })} + > +
{name}
+ {value === selectedOption && ( + + + + )} +
+ ))} +
+
+ {this.renderContent()} +
+
+
+
+ ) + } +} + +export default connect( + ({ metamask: { selectedAddress } }) => ({ + address: selectedAddress, + }), + dispatch => ({ + goToCoinbase: address => dispatch(buyEth({ network: '1', address, amount: 0 })), + showAccountDetail: address => dispatch(showAccountDetail(address)), + }) +)(BuyEtherScreen) diff --git a/mascara/src/app/first-time/create-password-screen.js b/mascara/src/app/first-time/create-password-screen.js new file mode 100644 index 000000000..d1a2ec70f --- /dev/null +++ b/mascara/src/app/first-time/create-password-screen.js @@ -0,0 +1,135 @@ +import EventEmitter from 'events' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import {createNewVaultAndKeychain} from '../../../../ui/app/actions' +import LoadingScreen from './loading-screen' +import Breadcrumbs from './breadcrumbs' +import Mascot from '../../../../ui/app/components/mascot' + +class CreatePasswordScreen extends Component { + static propTypes = { + isLoading: PropTypes.bool.isRequired, + createAccount: PropTypes.func.isRequired, + goToImportWithSeedPhrase: PropTypes.func.isRequired, + goToImportAccount: PropTypes.func.isRequired, + next: PropTypes.func.isRequired, + } + + state = { + password: '', + confirmPassword: '', + } + + constructor () { + super() + this.animationEventEmitter = new EventEmitter() + } + + isValid () { + const {password, confirmPassword} = this.state + + if (!password || !confirmPassword) { + return false + } + + if (password.length < 8) { + return false + } + + return password === confirmPassword + } + + createAccount = () => { + if (!this.isValid()) { + return + } + + const {password} = this.state + const {createAccount, next} = this.props + + createAccount(password) + .then(next) + } + + render () { + const { isLoading, goToImportWithSeedPhrase } = this.props + + return isLoading + ? + : ( +
+

Warning: This is Experimental software and is a Developer BETA

+
+
+ +
+ MetaMask is a secure identity vault for Ethereum. +
+
+ It allows you to hold ether & tokens, and interact with decentralized applications. +
+
+
+
+ Create Password +
+ this.setState({password: e.target.value})} + /> + this.setState({confirmPassword: e.target.value})} + /> + + { + e.preventDefault() + goToImportWithSeedPhrase() + }} + > + Import with seed phrase + + { /* } + { + e.preventDefault() + goToImportAccount() + }} + > + Import an account + + { */ } + +
+
+
+ ) + } +} + +export default connect( + ({ appState: { isLoading } }) => ({ isLoading }), + dispatch => ({ + createAccount: password => dispatch(createNewVaultAndKeychain(password)), + }) +)(CreatePasswordScreen) diff --git a/mascara/src/app/first-time/import-account-screen.js b/mascara/src/app/first-time/import-account-screen.js new file mode 100644 index 000000000..ab0aca0f0 --- /dev/null +++ b/mascara/src/app/first-time/import-account-screen.js @@ -0,0 +1,204 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import classnames from 'classnames' +import LoadingScreen from './loading-screen' +import {importNewAccount, hideWarning} from '../../../../ui/app/actions' + +const Input = ({ label, placeholder, onChange, errorMessage, type = 'text' }) => ( +
+
{label}
+ +
{errorMessage}
+
+) + +Input.prototype.propTypes = { + label: PropTypes.string.isRequired, + placeholder: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + errorMessage: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} + +class ImportAccountScreen extends Component { + static OPTIONS = { + PRIVATE_KEY: 'private_key', + JSON_FILE: 'json_file', + }; + + static propTypes = { + warning: PropTypes.string, + back: PropTypes.func.isRequired, + next: PropTypes.func.isRequired, + importNewAccount: PropTypes.func.isRequired, + hideWarning: PropTypes.func.isRequired, + isLoading: PropTypes.bool.isRequired, + }; + + state = { + selectedOption: ImportAccountScreen.OPTIONS.PRIVATE_KEY, + privateKey: '', + jsonFile: {}, + } + + isValid () { + const { OPTIONS } = ImportAccountScreen + const { privateKey, jsonFile, password } = this.state + + switch (this.state.selectedOption) { + case OPTIONS.JSON_FILE: + return Boolean(jsonFile && password) + case OPTIONS.PRIVATE_KEY: + default: + return Boolean(privateKey) + } + } + + onClick = () => { + const { OPTIONS } = ImportAccountScreen + const { importNewAccount, next } = this.props + const { privateKey, jsonFile, password } = this.state + + switch (this.state.selectedOption) { + case OPTIONS.JSON_FILE: + return importNewAccount('JSON File', [ jsonFile, password ]) + .then(next) + case OPTIONS.PRIVATE_KEY: + default: + return importNewAccount('Private Key', [ privateKey ]) + .then(next) + } + } + + renderPrivateKey () { + return Input({ + label: 'Add Private Key String', + placeholder: 'Enter private key', + onChange: e => this.setState({ privateKey: e.target.value }), + errorMessage: this.props.warning && 'Something went wrong. Please make sure your private key is correct.', + }) + } + + renderJsonFile () { + const { jsonFile: { name } } = this.state + const { warning } = this.props + + return ( +
+
+
Upload File
+
+ this.setState({ jsonFile: e.target.files[0] })} + /> + +
{name}
+
+
+ {warning && 'Something went wrong. Please make sure your JSON file is properly formatted.'} +
+
+ {Input({ + label: 'Enter Password', + placeholder: 'Enter Password', + type: 'password', + onChange: e => this.setState({ password: e.target.value }), + errorMessage: warning && 'Please make sure your password is correct.', + })} +
+ ) + } + + renderContent () { + const { OPTIONS } = ImportAccountScreen + + switch (this.state.selectedOption) { + case OPTIONS.JSON_FILE: + return this.renderJsonFile() + case OPTIONS.PRIVATE_KEY: + default: + return this.renderPrivateKey() + } + } + + render () { + const { OPTIONS } = ImportAccountScreen + const { selectedOption } = this.state + + return this.props.isLoading + ? + : ( +
+ { + e.preventDefault() + this.props.back() + }} + href="#" + > + {`< Back`} + +
+ Import an Account +
+
+ How would you like to import your account? +
+ + {this.renderContent()} + + + File import not working? + +
+ ) + } +} + +export default connect( + ({ appState: { isLoading, warning } }) => ({ isLoading, warning }), + dispatch => ({ + importNewAccount: (strategy, args) => dispatch(importNewAccount(strategy, args)), + hideWarning: () => dispatch(hideWarning()), + }) +)(ImportAccountScreen) diff --git a/mascara/src/app/first-time/import-seed-phrase-screen.js b/mascara/src/app/first-time/import-seed-phrase-screen.js new file mode 100644 index 000000000..93c3f9203 --- /dev/null +++ b/mascara/src/app/first-time/import-seed-phrase-screen.js @@ -0,0 +1,130 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import LoadingScreen from './loading-screen' +import { + createNewVaultAndRestore, + hideWarning, + displayWarning, + unMarkPasswordForgotten, + clearNotices, +} from '../../../../ui/app/actions' + +class ImportSeedPhraseScreen extends Component { + static propTypes = { + warning: PropTypes.string, + back: PropTypes.func.isRequired, + next: PropTypes.func.isRequired, + createNewVaultAndRestore: PropTypes.func.isRequired, + hideWarning: PropTypes.func.isRequired, + isLoading: PropTypes.bool.isRequired, + displayWarning: PropTypes.func, + }; + + state = { + seedPhrase: '', + password: '', + confirmPassword: '', + } + + onClick = () => { + const { password, seedPhrase, confirmPassword } = this.state + const { createNewVaultAndRestore, next, displayWarning, leaveImportSeedScreenState } = this.props + + if (seedPhrase.split(' ').length !== 12) { + this.warning = 'Seed Phrases are 12 words long' + displayWarning(this.warning) + return + } + + if (password.length < 8) { + this.warning = 'Passwords require a mimimum length of 8' + displayWarning(this.warning) + return + } + + if (password !== confirmPassword) { + this.warning = 'Confirmed password does not match' + displayWarning(this.warning) + return + } + this.warning = null + leaveImportSeedScreenState() + createNewVaultAndRestore(password, seedPhrase) + .then(next) + } + + render () { + return this.props.isLoading + ? + : ( +
+ { + e.preventDefault() + this.props.back() + }} + href="#" + > + {`< Back`} + +
+ Import an Account with Seed Phrase +
+
+ Enter your secret twelve word phrase here to restore your vault. +
+
+ + ",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var pa=d.documentElement,qa=/^key/,ra=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,sa=/^([^.]*)(?:\.(.+)|)/;function ta(){return!0}function ua(){return!1}function va(){try{return d.activeElement}catch(a){}}function wa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)wa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ua;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(pa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=sa.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c-1:r.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h\x20\t\r\n\f]*)[^>]*)\/>/gi,ya=/\s*$/g;function Ca(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Da(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ea(a){var b=Aa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&za.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(m&&(e=oa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(la(e,"script"),Da),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=la(h),f=la(a),d=0,e=f.length;d0&&ma(g,!i&&la(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(la(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!ya.test(a)&&!ka[(ia.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Xa(a,b,c,d,e){return new Xa.prototype.init(a,b,c,d,e)}r.Tween=Xa,Xa.prototype={constructor:Xa,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Xa.propHooks[this.prop];return a&&a.get?a.get(this):Xa.propHooks._default.get(this)},run:function(a){var b,c=Xa.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Xa.propHooks._default.set(this),this}},Xa.prototype.init.prototype=Xa.prototype,Xa.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Xa.propHooks.scrollTop=Xa.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Xa.prototype.init,r.fx.step={};var Ya,Za,$a=/^(?:toggle|show|hide)$/,_a=/queueHooks$/;function ab(){Za&&(a.requestAnimationFrame(ab),r.fx.tick())}function bb(){return a.setTimeout(function(){Ya=void 0}),Ya=r.now()}function cb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=aa[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function db(a,b,c){for(var d,e=(gb.tweeners[b]||[]).concat(gb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?hb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K); +if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),hb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ib[b]||r.find.attr;ib[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ib[g],ib[g]=e,e=null!=c(a,b,d)?g:null,ib[g]=f),e}});var jb=/^(?:input|select|textarea|button)$/i,kb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):jb.test(a.nodeName)||kb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});var lb=/[\t\r\n\f]/g;function mb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,mb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,mb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=mb(c),d=1===c.nodeType&&(" "+e+" ").replace(lb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=r.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,mb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=mb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(c)+" ").replace(lb," ").indexOf(b)>-1)return!0;return!1}});var nb=/\r/g,ob=/[\x20\t\r\n\f]+/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(nb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:r.trim(r.text(a)).replace(ob," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type,g=f?null:[],h=f?e+1:d.length,i=e<0?h:f?e:0;i-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ha.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,""),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("