diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a9523b3..e4dd7ae98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Current Master +## 4.6.1 Mon Apr 30 2018 + +- Fix bug where sending a transaction resulted in an infinite spinner +- Allow transactions with a 0 gwei gas price +- Handle encoding errors in ERC20 symbol + digits +- Fix ShapeShift forms (new + old ui) +- Fix sourcemaps + ## 4.6.0 Thu Apr 26 2018 - Correctly format currency conversion for locally selected preferred currency. diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3b20ab49a..a40c2635c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -98,6 +98,9 @@ "clickCopy": { "message": "Click to Copy" }, + "close": { + "message": "Close" + }, "confirm": { "message": "Confirm" }, @@ -259,6 +262,9 @@ "enterPasswordConfirm": { "message": "Enter your password to confirm" }, + "enterPasswordContinue": { + "message": "Enter password to continue" + }, "passwordNotLongEnough": { "message": "Password not long enough" }, @@ -331,6 +337,9 @@ "gasPriceRequired": { "message": "Gas Price Required" }, + "generatingTransaction": { + "message": "Generating transaction" + }, "getEther": { "message": "Get Ether" }, @@ -476,6 +485,9 @@ "metamaskDescription": { "message": "MetaMask is a secure identity vault for Ethereum." }, + "metamaskSeedWords": { + "message": "MetaMask Seed Words" + }, "min": { "message": "Minimum" }, @@ -549,6 +561,9 @@ "message": "or", "description": "choice between creating or importing a new account" }, + "password": { + "message": "Password" + }, "passwordCorrect": { "message": "Please make sure your password is correct." }, @@ -634,8 +649,17 @@ "revealSeedWords": { "message": "Reveal Seed Words" }, + "revealSeedWordsTitle": { + "message": "Seed Phrase" + }, + "revealSeedWordsDescription": { + "message": "If you ever change browsers or move computers, you will need this seed phrase to access your accounts. Save them somewhere safe and secret." + }, + "revealSeedWordsWarningTitle": { + "message": "DO NOT share this phrase with anyone!" + }, "revealSeedWordsWarning": { - "message": "Do not recover your seed words in a public place! These words can be used to steal all your accounts." + "message": "These words can be used to steal all your accounts." }, "revert": { "message": "Revert" @@ -677,6 +701,9 @@ "reprice_subtitle": { "message": "Increase your gas price to attempt to overwrite and speed up your transaction" }, + "saveAsCsvFile": { + "message": "Save as CSV File" + }, "saveAsFile": { "message": "Save as File", "description": "Account export process" @@ -909,7 +936,7 @@ "youSign": { "message": "You are signing" }, - "generatingTransaction": { - "message": "Generating transaction" + "yourPrivateSeedPhrase": { + "message": "Your private seed phrase" } } diff --git a/app/images/copy-to-clipboard.svg b/app/images/copy-to-clipboard.svg new file mode 100644 index 000000000..c67c2aa84 --- /dev/null +++ b/app/images/copy-to-clipboard.svg @@ -0,0 +1,24 @@ + + + + 374E58A5-C29E-4921-83E7-889FA06D6408 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/download.svg b/app/images/download.svg index 137a1190e..b55066414 100644 --- a/app/images/download.svg +++ b/app/images/download.svg @@ -1,15 +1,26 @@ - - - - - - - - - + + + + 50559280-0739-419A-8E87-3CDD16A6996A + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/warning.svg b/app/images/warning.svg new file mode 100644 index 000000000..9c8d697d7 --- /dev/null +++ b/app/images/warning.svg @@ -0,0 +1,22 @@ + + + + Group 7 + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/manifest.json b/app/manifest.json index 3e5eed205..8a14323f0 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.6.0", + "version": "4.6.1", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index abeec4cc0..87d716aa6 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,4 +1,5 @@ const ObservableStore = require('obs-store') +const { warn } = require('loglevel') // By default, poll every 3 minutes const DEFAULT_INTERVAL = 180 * 1000 @@ -42,7 +43,10 @@ class TokenRatesController { const response = await fetch(`https://metamask.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`) const json = await response.json() return json && json.length ? json[0].averagePrice : 0 - } catch (error) { } + } catch (error) { + warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error) + return 0 + } } /** diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index c10ff2f4e..221746467 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -101,6 +101,7 @@ ConfigManager.prototype.setShowSeedWords = function (should) { this.setData(data) } + ConfigManager.prototype.getShouldShowSeedWords = function () { var data = this.getData() return data.showSeedWords @@ -116,27 +117,6 @@ ConfigManager.prototype.getSeedWords = function () { var data = this.getData() return data.seedWords } - -/** - * Called to set the isRevealingSeedWords flag. This happens only when the user chooses to reveal - * the seed words and not during the first time flow. - * @param {boolean} reveal - Value to set the isRevealingSeedWords flag. - */ -ConfigManager.prototype.setIsRevealingSeedWords = function (reveal = false) { - const data = this.getData() - data.isRevealingSeedWords = reveal - this.setData(data) -} - -/** - * Returns the isRevealingSeedWords flag. - * @returns {boolean|undefined} - */ -ConfigManager.prototype.getIsRevealingSeedWords = function () { - const data = this.getData() - return data.isRevealingSeedWords -} - ConfigManager.prototype.setRpcTarget = function (rpcUrl) { var config = this.getConfig() config.provider = { diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 48b941c3d..b1b67f771 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -23,23 +23,16 @@ function setupRaven(opts) { release, transport: function(opts) { const report = opts.data - // simplify certain complex error messages - if (report.exception && report.exception.values) { - report.exception.values.forEach(item => { - let errorMessage = item.value - // simplify ethjs error messages - errorMessage = extractEthjsErrorMessage(errorMessage) - // simplify 'Transaction Failed: known transaction' - if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) { - // cut the hash from the error message - errorMessage = 'Transaction Failed: known transaction' - } - // finalize - item.value = errorMessage - }) + try { + // handle error-like non-error exceptions + nonErrorException(report) + // simplify certain complex error messages (e.g. Ethjs) + simplifyErrorMessages(report) + // modify report urls + rewriteReportUrls(report) + } catch (err) { + console.warn(err) } - // modify report urls - rewriteReportUrls(report) // make request normally client._makeRequest(opts) }, @@ -49,6 +42,31 @@ function setupRaven(opts) { return Raven } +function nonErrorException(report) { + // handle errors that lost their error-ness in serialization + if (report.message.includes('Non-Error exception captured with keys: message')) { + if (!(report.extra && report.extra.__serialized__)) return + report.message = `Non-Error Exception: ${report.extra.__serialized__.message}` + } +} + +function simplifyErrorMessages(report) { + if (report.exception && report.exception.values) { + report.exception.values.forEach(item => { + let errorMessage = item.value + // simplify ethjs error messages + errorMessage = extractEthjsErrorMessage(errorMessage) + // simplify 'Transaction Failed: known transaction' + if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) { + // cut the hash from the error message + errorMessage = 'Transaction Failed: known transaction' + } + // finalize + item.value = errorMessage + }) + } +} + function rewriteReportUrls(report) { // update request url report.request.url = toMetamaskUrl(report.request.url) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index edde38819..c4a73d8ea 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -309,7 +309,6 @@ module.exports = class MetamaskController extends EventEmitter { lostAccounts: this.configManager.getLostAccounts(), seedWords: this.configManager.getSeedWords(), forgottenPassword: this.configManager.getPasswordForgotten(), - isRevealingSeedWords: Boolean(this.configManager.getIsRevealingSeedWords()), }, } } @@ -351,7 +350,6 @@ module.exports = class MetamaskController extends EventEmitter { clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), importAccountWithStrategy: this.importAccountWithStrategy.bind(this), - setIsRevealingSeedWords: this.configManager.setIsRevealingSeedWords.bind(this.configManager), // vault management submitPassword: nodeify(keyringController.submitPassword, keyringController), diff --git a/development/sourcemap-validator.js b/development/sourcemap-validator.js new file mode 100644 index 000000000..edc97667a --- /dev/null +++ b/development/sourcemap-validator.js @@ -0,0 +1,49 @@ +const fs = require('fs') +const { SourceMapConsumer } = require('source-map') + +// +// Utility to help check if sourcemaps are working +// +// searches `dist/chrome/inpage.js` for "new Error" statements +// and prints their source lines using the sourcemaps. +// if not working it may error or print minified garbage +// + +start() + +async function start() { + const rawBuild = fs.readFileSync(__dirname + '/../dist/chrome/inpage.js', 'utf8') + const rawSourceMap = fs.readFileSync(__dirname + '/../dist/sourcemaps/inpage.js.map', 'utf8') + const consumer = await new SourceMapConsumer(rawSourceMap) + + console.log('hasContentsOfAllSources:', consumer.hasContentsOfAllSources(), '\n') + console.log('sources:') + consumer.sources.map((sourcePath) => console.log(sourcePath)) + + console.log('\nexamining "new Error" statements:\n') + const sourceLines = rawBuild.split('\n') + sourceLines.map(line => indicesOf('new Error', line)) + .forEach((errorIndices, lineIndex) => { + // if (errorIndex === null) return console.log('line does not contain "new Error"') + errorIndices.forEach((errorIndex) => { + const position = { line: lineIndex + 1, column: errorIndex } + const result = consumer.originalPositionFor(position) + if (!result.source) return console.warn(`!! missing source for position: ${position}`) + // filter out deps distributed minified without sourcemaps + if (result.source === 'node_modules/browserify/node_modules/browser-pack/_prelude.js') return // minified mess + if (result.source === 'node_modules/web3/dist/web3.min.js') return // minified mess + const sourceContent = consumer.sourceContentFor(result.source) + const sourceLines = sourceContent.split('\n') + const line = sourceLines[result.line-1] + console.log(`\n========================== ${result.source} ====================================\n`) + console.log(line) + console.log(`\n==============================================================================\n`) + }) + }) +} + +function indicesOf(substring, string) { + var a=[],i=-1; + while((i=string.indexOf(substring,i+1)) >= 0) a.push(i); + return a; +} diff --git a/gulpfile.js b/gulpfile.js index 4f0da9d60..4dca7089f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -247,7 +247,7 @@ gulp.task('dev:scss', createScssBuildTask({ src: 'ui/app/css/index.scss', dest: 'ui/app/css/output', devMode: true, - pattern: 'ui/app/css/**/*.scss', + pattern: 'ui/app/**/*.scss', })) function createScssBuildTask({ src, dest, devMode, pattern }) { @@ -484,16 +484,6 @@ function generateBundler(opts, performBundle) { NODE_ENV: opts.devMode ? 'development' : 'production', })) - // Minification - if (opts.minifyBuild) { - bundler.transform('uglifyify', { - global: true, - mangle: { - reserved: [ 'MetamaskInpageProvider' ] - }, - }) - } - if (opts.watch) { bundler = watchify(bundler) // on any file update, re-runs the bundler @@ -567,6 +557,16 @@ function bundleTask(opts) { .pipe(sourcemaps.init({ loadMaps: true })) } + // Minification + if (opts.minifyBuild) { + buildStream = buildStream + .pipe(uglify({ + mangle: { + reserved: [ 'MetamaskInpageProvider' ] + }, + })) + } + // Finalize Source Maps (writes .map file) if (opts.buildSourceMaps) { buildStream = buildStream diff --git a/mascara/src/app/first-time/seed-screen.js b/mascara/src/app/first-time/seed-screen.js index 9af9ca3be..d004be77b 100644 --- a/mascara/src/app/first-time/seed-screen.js +++ b/mascara/src/app/first-time/seed-screen.js @@ -8,7 +8,6 @@ import Identicon from '../../../../ui/app/components/identicon' import Breadcrumbs from './breadcrumbs' import LoadingScreen from './loading-screen' import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes' -import { confirmSeedWords } from '../../../../ui/app/actions' const LockIcon = props => ( clearSeedWords().then(() => history.push(DEFAULT_ROUTE))} - disabled={!isShowingSecret} - > - Done - - : - } - renderSecretScreen () { - const { isRevealingSeedWords } = this.props + const { isShowingSecret } = this.state + const { history } = this.props return (
@@ -152,8 +121,14 @@ class BackupPhraseScreen extends Component {
- { this.renderSubmitButton() } - { !isRevealingSeedWords && } + +
) @@ -175,25 +150,13 @@ class BackupPhraseScreen extends Component { } } -const mapStateToProps = ({ metamask, appState }) => { - const { selectedAddress, seedWords, isRevealingSeedWords } = metamask - const { isLoading } = appState - - return { - seedWords, - isRevealingSeedWords, - isLoading, - address: selectedAddress, - } -} - -const mapDispatchToProps = dispatch => { - return { - clearSeedWords: () => dispatch(confirmSeedWords()), - } -} - export default compose( withRouter, - connect(mapStateToProps, mapDispatchToProps), + connect( + ({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({ + seedWords, + isLoading, + address: selectedAddress, + }) + ) )(BackupPhraseScreen) diff --git a/old-ui/app/components/pending-tx.js b/old-ui/app/components/pending-tx.js index cd4189fc4..c8132539c 100644 --- a/old-ui/app/components/pending-tx.js +++ b/old-ui/app/components/pending-tx.js @@ -16,8 +16,7 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const BNInput = require('./bn-as-decimal-input') -// corresponds with 0.1 GWEI -const MIN_GAS_PRICE_BN = new BN('100000000') +const MIN_GAS_PRICE_BN = new BN('0') const MIN_GAS_LIMIT_BN = new BN('21000') module.exports = PendingTx diff --git a/old-ui/app/components/shapeshift-form.js b/old-ui/app/components/shapeshift-form.js index a54987c04..97068db0a 100644 --- a/old-ui/app/components/shapeshift-form.js +++ b/old-ui/app/components/shapeshift-form.js @@ -138,7 +138,7 @@ ShapeshiftForm.prototype.renderMain = function () { width: '229px', height: '82px', }, - }, this.props.warning) + }, this.props.warning + '') : this.renderInfo(), this.renderRefundAddressForCoin(coin), diff --git a/package-lock.json b/package-lock.json index 5b5b82450..884f53ce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2821,6 +2821,20 @@ "ieee754": "1.1.8" } }, + "buffer-alloc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.1.0.tgz", + "integrity": "sha1-BVFNM78WVtNUDGhPZbEgLpDsowM=", + "requires": { + "buffer-alloc-unsafe": "0.1.1", + "buffer-fill": "0.1.1" + } + }, + "buffer-alloc-unsafe": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz", + "integrity": "sha1-/+H2dVHdBVc33iUzN7/oU9+rGmo=" + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -2831,6 +2845,11 @@ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" }, + "buffer-fill": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-0.1.1.tgz", + "integrity": "sha512-YgBMBzdRLEfgxJIGu2wrvI2E03tMCFU1p7d1KhB4BOoMN0VxmTFjSyN5JtKt9z8Z9JajMHruI6SE25W96wNv7Q==" + }, "buffer-from": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", @@ -3603,6 +3622,14 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": "1.0.1" + } } } }, @@ -4350,7 +4377,7 @@ "requires": { "file-type": "5.2.0", "is-stream": "1.1.0", - "tar-stream": "1.5.5" + "tar-stream": "1.6.0" } }, "decompress-tarbz2": { @@ -4833,6 +4860,15 @@ "string_decoder": "0.10.31" } }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -5870,25 +5906,26 @@ "resolved": "https://registry.npmjs.org/eth-json-rpc-infura/-/eth-json-rpc-infura-3.0.0.tgz", "integrity": "sha512-Ab6170AxlF4DK+HDImh52+AetwHPHstgg8uWtX4im26rqK7u4ziSfvUIUK2+/LK0pi0wbIFb8hZm5jPKAXDmBA==", "requires": { - "eth-json-rpc-middleware": "1.5.0", + "eth-json-rpc-middleware": "1.6.0", "json-rpc-engine": "3.6.1", "json-rpc-error": "2.0.0", "tape": "4.8.0" } }, "eth-json-rpc-middleware": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-1.5.0.tgz", - "integrity": "sha1-FrEFM4aqOAOxJXMqpt4H6t8Ghyk=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eth-json-rpc-middleware/-/eth-json-rpc-middleware-1.6.0.tgz", + "integrity": "sha512-tDVCTlrUvdqHKqivYMjtFZsdD7TtpNLBCfKAcOpaVs7orBMS/A8HWro6dIzNtTZIR05FAbJ3bioFOnZpuCew9Q==", "requires": { "async": "2.6.0", "eth-query": "2.1.2", - "eth-tx-summary": "3.1.2", + "eth-tx-summary": "3.2.1", "ethereumjs-block": "1.7.0", "ethereumjs-tx": "1.3.3", - "ethereumjs-util": "5.1.5", + "ethereumjs-util": "5.2.0", "ethereumjs-vm": "2.3.2", "fetch-ponyfill": "4.1.0", + "json-rpc-engine": "3.6.1", "json-rpc-error": "2.0.0", "json-stable-stringify": "1.0.1", "promise-to-callback": "1.0.0", @@ -5896,9 +5933,9 @@ }, "dependencies": { "ethereumjs-util": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.5.tgz", - "integrity": "sha512-xPaSEATYJpMTCGowIt0oMZwFP4R1bxd6QsWgkcDvFL0JtXsr39p32WEcD14RscCjfP41YXZPCVWA4yAg0nrJmw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", "requires": { "bn.js": "4.11.8", "create-hash": "1.1.3", @@ -6226,11 +6263,12 @@ } }, "eth-tx-summary": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eth-tx-summary/-/eth-tx-summary-3.1.2.tgz", - "integrity": "sha1-44g2/J+LVvFNdZUvD15XD4j7IiA=", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/eth-tx-summary/-/eth-tx-summary-3.2.1.tgz", + "integrity": "sha512-mu8g5tDkQxlFah58ggFhTzolE4OnYTj6j8SVsnGsiWT7WxN722RwnEsk/bco2foy+PLSEF2Mnoiw+wCqKoY72A==", "requires": { "async": "2.6.0", + "bn.js": "4.11.8", "clone": "2.1.1", "concat-stream": "1.6.0", "end-of-stream": "1.4.0", @@ -6238,12 +6276,46 @@ "ethereumjs-block": "1.7.0", "ethereumjs-tx": "1.3.3", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", - "ethereumjs-vm": "2.3.2", + "ethereumjs-vm": "2.3.5", "through2": "2.0.3", "treeify": "1.1.0", "web3-provider-engine": "13.8.0" }, "dependencies": { + "ethereumjs-vm": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.3.5.tgz", + "integrity": "sha512-AJ7x44+xqyE5+UO3Nns19WkTdZfyqFZ+sEjIEpvme7Ipbe3iBU1uwCcHEdiu/yY9bdhr3IfSa/NfIKNeXPaRVQ==", + "requires": { + "async": "2.6.0", + "async-eventemitter": "0.2.4", + "ethereum-common": "0.2.0", + "ethereumjs-account": "2.0.4", + "ethereumjs-block": "1.7.0", + "ethereumjs-util": "5.2.0", + "fake-merkle-patricia-tree": "1.0.1", + "functional-red-black-tree": "1.0.1", + "merkle-patricia-tree": "2.3.0", + "rustbn.js": "0.1.1", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "4.11.8", + "create-hash": "1.1.3", + "ethjs-util": "0.1.4", + "keccak": "1.4.0", + "rlp": "2.0.0", + "safe-buffer": "5.1.1", + "secp256k1": "3.4.0" + } + } + } + }, "web3-provider-engine": { "version": "13.8.0", "resolved": "https://registry.npmjs.org/web3-provider-engine/-/web3-provider-engine-13.8.0.tgz", @@ -6255,8 +6327,8 @@ "eth-sig-util": "1.4.2", "ethereumjs-block": "1.7.0", "ethereumjs-tx": "1.3.3", - "ethereumjs-util": "5.1.5", - "ethereumjs-vm": "2.3.2", + "ethereumjs-util": "5.2.0", + "ethereumjs-vm": "2.3.5", "fetch-ponyfill": "4.1.0", "json-rpc-error": "2.0.0", "json-stable-stringify": "1.0.1", @@ -6271,9 +6343,9 @@ }, "dependencies": { "ethereumjs-util": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.5.tgz", - "integrity": "sha512-xPaSEATYJpMTCGowIt0oMZwFP4R1bxd6QsWgkcDvFL0JtXsr39p32WEcD14RscCjfP41YXZPCVWA4yAg0nrJmw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", "requires": { "bn.js": "4.11.8", "create-hash": "1.1.3", @@ -7790,6 +7862,11 @@ "null-check": "1.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-exists-sync": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", @@ -8871,7 +8948,7 @@ "ethereumjs-account": "2.0.4", "ethereumjs-block": "1.2.2", "ethereumjs-tx": "1.3.3", - "ethereumjs-util": "5.1.5", + "ethereumjs-util": "5.2.0", "ethereumjs-vm": "2.3.3", "ethereumjs-wallet": "0.6.0", "fake-merkle-patricia-tree": "1.0.1", @@ -8892,7 +8969,7 @@ "tmp": "0.0.31", "web3": "1.0.0-beta.34", "web3-provider-engine": "13.8.0", - "websocket": "1.0.25", + "websocket": "1.0.26", "yargs": "7.1.0" }, "dependencies": { @@ -8992,9 +9069,9 @@ } }, "ethereumjs-util": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.5.tgz", - "integrity": "sha512-xPaSEATYJpMTCGowIt0oMZwFP4R1bxd6QsWgkcDvFL0JtXsr39p32WEcD14RscCjfP41YXZPCVWA4yAg0nrJmw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", "requires": { "bn.js": "4.11.6", "create-hash": "1.1.3", @@ -9015,7 +9092,7 @@ "ethereum-common": "0.2.0", "ethereumjs-account": "2.0.4", "ethereumjs-block": "1.7.1", - "ethereumjs-util": "5.1.5", + "ethereumjs-util": "5.2.0", "fake-merkle-patricia-tree": "1.0.1", "functional-red-black-tree": "1.0.1", "merkle-patricia-tree": "2.3.0", @@ -9036,7 +9113,7 @@ "async": "2.6.0", "ethereum-common": "0.2.0", "ethereumjs-tx": "1.3.3", - "ethereumjs-util": "5.1.5", + "ethereumjs-util": "5.2.0", "merkle-patricia-tree": "2.3.0" } } @@ -9174,7 +9251,7 @@ "eth-sig-util": "1.4.2", "ethereumjs-block": "1.2.2", "ethereumjs-tx": "1.3.3", - "ethereumjs-util": "5.1.5", + "ethereumjs-util": "5.2.0", "ethereumjs-vm": "2.3.3", "fetch-ponyfill": "4.1.0", "json-rpc-error": "2.0.0", @@ -10422,7 +10499,7 @@ "debug-fabulous": "1.0.0", "detect-newline": "2.1.0", "graceful-fs": "4.1.11", - "source-map": "0.4.4", + "source-map": "0.7.2", "strip-bom-string": "1.0.0", "through2": "2.0.3" }, @@ -11933,6 +12010,16 @@ "integrity": "sha1-Skxd2OT7Xps82mDIIt+tyu5m4K8=", "requires": { "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": "1.0.1" + } + } } }, "inquirer": { @@ -21856,6 +21943,16 @@ "requires": { "js-base64": "2.4.3", "source-map": "0.4.4" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": "1.0.1" + } + } } }, "secp256k1": { @@ -22718,12 +22815,10 @@ "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" }, "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": "1.0.1" - } + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.2.tgz", + "integrity": "sha512-NDJB/R2BS7YJG0tP9SbE4DKwKj1idLT5RJqfVYZ7dreFX7wulZT3xxVhbYKrQo9n0JkRptl51TrX/5VK3HodMA==", + "dev": true }, "source-map-resolve": { "version": "0.5.1", @@ -22920,7 +23015,7 @@ "requires": { "esprima": "1.0.4", "estraverse": "1.3.2", - "source-map": "0.4.4" + "source-map": "0.7.2" } }, "esprima": { @@ -24245,13 +24340,16 @@ } }, "tar-stream": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", - "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.0.tgz", + "integrity": "sha512-lh2iAPG/BHNmN6WB9Ybdynk9rEJ5GD/dy4zscHmVlwa1dq2tpE+BH78i5vjYwYVWEaOXGBjzxr89aVACF17Cpw==", "requires": { "bl": "1.2.1", + "buffer-alloc": "1.1.0", "end-of-stream": "1.4.0", + "fs-constants": "1.0.0", "readable-stream": "2.3.3", + "to-buffer": "1.1.1", "xtend": "4.0.1" } }, @@ -24639,6 +24737,11 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", @@ -24972,55 +25075,12 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "dev": true, - "requires": { - "commander": "2.13.0", - "source-map": "0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "uglify-to-browserify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", "optional": true }, - "uglifyify": { - "version": "github:MetaMask/uglifyify#8662585e39125a96a5379d71cb4a606829790f87", - "dev": true, - "requires": { - "convert-source-map": "1.1.3", - "extend": "1.3.0", - "minimatch": "3.0.4", - "through": "2.3.8", - "uglify-es": "3.3.9" - }, - "dependencies": { - "extend": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", - "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=", - "dev": true - } - } - }, "ultron": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", @@ -26284,7 +26344,7 @@ "ethereumjs-block": "1.7.0", "ethereumjs-tx": "1.3.3", "ethereumjs-util": "5.1.5", - "ethereumjs-vm": "2.3.4", + "ethereumjs-vm": "2.3.5", "json-rpc-error": "2.0.0", "json-stable-stringify": "1.0.1", "promise-to-callback": "1.0.0", @@ -26317,7 +26377,7 @@ "integrity": "sha512-uMYkEP6fga8CyNo8TMoA/7cxi6bL3V8pTvjKQikOi9iYl6/AO5xlfgniyAMElSiq2mmXz3lYa/9VYDMzt/J5aA==", "requires": { "cross-fetch": "2.1.0", - "eth-json-rpc-middleware": "1.5.0", + "eth-json-rpc-middleware": "1.6.0", "json-rpc-engine": "3.6.1", "json-rpc-error": "2.0.0", "tape": "4.8.0" @@ -26338,9 +26398,9 @@ } }, "ethereumjs-vm": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.3.4.tgz", - "integrity": "sha512-Y4SlzNDqxrCO58jhp98HdnZVdjOqB+HC0hoU+N/DEp1aU+hFkRX/nru5F7/HkQRPIlA6aJlQp/xIA6xZs1kspw==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.3.5.tgz", + "integrity": "sha512-AJ7x44+xqyE5+UO3Nns19WkTdZfyqFZ+sEjIEpvme7Ipbe3iBU1uwCcHEdiu/yY9bdhr3IfSa/NfIKNeXPaRVQ==", "requires": { "async": "2.6.0", "async-eventemitter": "0.2.4", @@ -27017,9 +27077,9 @@ } }, "websocket": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.25.tgz", - "integrity": "sha512-M58njvi6ZxVb5k7kpnHh2BvNKuBWiwIYvsToErBzWhvBZYwlEiLcyLrG41T1jRcrY9ettqPYEqduLI7ul54CVQ==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz", + "integrity": "sha512-fjcrYDPIQxpTnqFQ9JjxUQcdvR89MFAOjPBlF+vjOt49w/XW4fJknUoMz/mDIn2eK1AdslVojcaOxOqyZZV8rw==", "requires": { "debug": "2.6.9", "nan": "2.8.0", diff --git a/package.json b/package.json index 73892bc28..4528ca31b 100644 --- a/package.json +++ b/package.json @@ -217,9 +217,10 @@ "eslint-plugin-json": "^1.2.0", "eslint-plugin-mocha": "^5.0.0", "eslint-plugin-react": "^7.4.0", - "eth-json-rpc-middleware": "^1.2.7", + "eth-json-rpc-middleware": "^1.6.0", "fs-promise": "^2.0.3", "ganache-cli": "^6.1.0", + "ganache-core": "^2.1.0", "gifencoder": "^1.1.0", "gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed", "gulp-babel": "^7.0.0", @@ -269,10 +270,10 @@ "selenium-webdriver": "^3.5.0", "shell-parallel": "^1.0.3", "sinon": "^5.0.0", + "source-map": "^0.7.2", "stylelint-config-standard": "^18.2.0", "tape": "^4.5.1", "testem": "^2.0.0", - "uglifyify": "github:MetaMask/uglifyify#keep-flags", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", "watchify": "^3.9.0" diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 09a074750..3da3f4f95 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -23,6 +23,37 @@ global.ethQuery = { global.ethereumProvider = {} +async function customizeGas (assert, price, limit, ethFee, usdFee) { + const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container') + sendGasOpenCustomizeModalButton[0].click() + + const customizeGasModal = await queryAsync($, '.send-v2__customize-gas') + assert.ok(customizeGasModal[0], 'should render the customize gas modal') + + const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input') + customizeGasPriceInput.val(price) + reactTriggerChange(customizeGasPriceInput[0]) + const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input') + customizeGasLimitInput.val(limit) + reactTriggerChange(customizeGasLimitInput[0]) + + const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save') + customizeGasSaveButton[0].click() + const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') + + assert.equal( + (await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(), + ethFee, + 'send gas field should show customized gas total' + ) + + assert.equal( + (await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent, + usdFee, + 'send gas field should show customized gas total converted to USD' + ) +} + async function runSendFlowTest(assert, done) { console.log('*** start runSendFlowTest') const selectState = await queryAsync($, 'select') @@ -95,32 +126,8 @@ async function runSendFlowTest(assert, done) { 'send gas field should show estimated gas total converted to USD' ) - const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container') - sendGasOpenCustomizeModalButton[0].click() - - const customizeGasModal = await queryAsync($, '.send-v2__customize-gas') - assert.ok(customizeGasModal[0], 'should render the customize gas modal') - - const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input') - customizeGasPriceInput.val(50) - reactTriggerChange(customizeGasPriceInput[0]) - const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input') - customizeGasLimitInput.val(60000) - reactTriggerChange(customizeGasLimitInput[0]) - - const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save') - customizeGasSaveButton[0].click() - - assert.equal( - (await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(), - '0.003', - 'send gas field should show customized gas total' - ) - assert.equal( - (await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent, - '$3.60 USD', - 'send gas field should show customized gas total converted to USD' - ) + await customizeGas(assert, 0, 21000, '0', '$0.00 USD') + await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD') const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') diff --git a/test/stub/provider.js b/test/stub/provider.js index e77db4e28..a1c70486d 100644 --- a/test/stub/provider.js +++ b/test/stub/provider.js @@ -1,14 +1,28 @@ const JsonRpcEngine = require('json-rpc-engine') const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold') -const TestBlockchain = require('eth-block-tracker/test/util/testBlockMiddleware') +const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware') +const GanacheCore = require('ganache-core') module.exports = { createEngineForTestData, providerFromEngine, scaffoldMiddleware, createTestProviderTools, + getTestSeed, + getTestAccounts, } +function getTestSeed () { + return 'people carpet cluster attract ankle motor ozone mass dove original primary mask' +} + +function getTestAccounts () { + return [ + { address: '0x88bb7F89eB5e5b30D3e15a57C68DBe03C6aCCB21', key: Buffer.from('254A8D551474F35CCC816388B4ED4D20B945C96B7EB857A68064CB9E9FB2C092', 'hex') }, + { address: '0x1fe9aAB565Be19629fF4e8541ca2102fb42D7724', key: Buffer.from('6BAB5A4F2A6911AF8EE2BD32C6C05F6643AC48EF6C939CDEAAAE6B1620805A9B', 'hex') }, + { address: '0xbda5c89aa6bA1b352194291AD6822C92AbC87c7B', key: Buffer.from('9B11D7F833648F26CE94D544855558D7053ECD396E4F4563968C232C012879B0', 'hex') }, + ] +} function createEngineForTestData () { return new JsonRpcEngine() @@ -21,11 +35,13 @@ function providerFromEngine (engine) { function createTestProviderTools (opts = {}) { const engine = createEngineForTestData() - const testBlockchain = new TestBlockchain() // handle provided hooks engine.push(scaffoldMiddleware(opts.scaffold || {})) // handle block tracker methods - engine.push(testBlockchain.createMiddleware()) + engine.push(providerAsMiddleware(GanacheCore.provider({ + mnemonic: getTestSeed(), + }))) + // wrap in standard provider interface const provider = providerFromEngine(engine) - return { provider, engine, testBlockchain } + return { provider, engine } } diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js index adeca9b5f..18c3f9ab9 100644 --- a/test/unit/metamask-controller-test.js +++ b/test/unit/metamask-controller-test.js @@ -6,6 +6,12 @@ const MetaMaskController = require('../../app/scripts/metamask-controller') const blacklistJSON = require('../stub/blacklist') const firstTimeState = require('../../app/scripts/first-time-state') +const DEFAULT_LABEL = 'Account 1' +const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' +const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' +const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' +const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' + describe('MetaMaskController', function () { let metamaskController const sandbox = sinon.sandbox.create() @@ -87,17 +93,28 @@ describe('MetaMaskController', function () { describe('#createNewVaultAndRestore', function () { it('should be able to call newVaultAndRestore despite a mistake.', async function () { - const password = 'what-what-what' - const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu' - const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' - await metamaskController.createNewVaultAndRestore(password, wrongSeed) - .catch((e) => { - return - }) - await metamaskController.createNewVaultAndRestore(password, rightSeed) + await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null) + await metamaskController.createNewVaultAndRestore(password, TEST_SEED) assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) }) + + it('should clear previous identities after vault restoration', async () => { + await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) + assert.deepEqual(metamaskController.getState().identities, { + [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, + }) + + await metamaskController.keyringController.saveAccountLabel(TEST_ADDRESS, 'Account Foo') + assert.deepEqual(metamaskController.getState().identities, { + [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, + }) + + await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) + assert.deepEqual(metamaskController.getState().identities, { + [TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL }, + }) + }) }) }) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 20d6f8573..0b5c7226a 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -6,16 +6,15 @@ const ObservableStore = require('obs-store') const sinon = require('sinon') const TransactionController = require('../../app/scripts/controllers/transactions') const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils') -const { createTestProviderTools } = require('../stub/provider') +const { createTestProviderTools, getTestAccounts } = require('../stub/provider') const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 -const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') describe('Transaction Controller', function () { - let txController, provider, providerResultStub, testBlockchain + let txController, provider, providerResultStub, query, fromAccount beforeEach(function () { providerResultStub = { @@ -24,9 +23,9 @@ describe('Transaction Controller', function () { // by default, all accounts are external accounts (not contracts) eth_getCode: '0x', } - const providerTools = createTestProviderTools({ scaffold: providerResultStub }) - provider = providerTools.provider - testBlockchain = providerTools.testBlockchain + provider = createTestProviderTools({ scaffold: providerResultStub }).provider + query = new EthjsQuery(provider) + fromAccount = getTestAccounts()[0] txController = new TransactionController({ provider, @@ -34,7 +33,7 @@ describe('Transaction Controller', function () { txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, signTransaction: (ethTx) => new Promise((resolve) => { - ethTx.sign(privKey) + ethTx.sign(fromAccount.key) resolve() }), }) @@ -298,12 +297,12 @@ describe('Transaction Controller', function () { describe('#updateAndApproveTransaction', function () { let txMeta - beforeEach(function () { + beforeEach(() => { txMeta = { id: 1, status: 'unapproved', txParams: { - from: '0xc684832530fcbddae4b4230a47e991ddcec2831d', + from: fromAccount.address, to: '0x1678a085c290ebd122dc42cba69373b5953b831d', gasPrice: '0x77359400', gas: '0x7b0d', @@ -312,11 +311,12 @@ describe('Transaction Controller', function () { metamaskNetworkId: currentNetworkId, } }) - it('should update and approve transactions', function () { + it('should update and approve transactions', async () => { txController.txStateManager.addTx(txMeta) - txController.updateAndApproveTransaction(txMeta) + const approvalPromise = txController.updateAndApproveTransaction(txMeta) const tx = txController.txStateManager.getTx(1) assert.equal(tx.status, 'approved') + await approvalPromise }) }) diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index ae7afd285..be16225ba 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -95,4 +95,4 @@ describe('txUtils', function () { txUtils.validateFrom(txParams) }) }) -}) \ No newline at end of file +}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 81d9c333b..62875c629 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -83,7 +83,7 @@ var actions = { REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', revealSeedConfirmation: revealSeedConfirmation, requestRevealSeed: requestRevealSeed, - + requestRevealSeedWords, // unlock screen UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', UNLOCK_FAILED: 'UNLOCK_FAILED', @@ -345,11 +345,13 @@ function transitionBackward () { } } -function clearSeedWordCache () { - log.debug(`background.clearSeedWordCache`) +function confirmSeedWords () { return dispatch => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) return new Promise((resolve, reject) => { background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) if (err) { dispatch(actions.displayWarning(err.message)) return reject(err) @@ -363,22 +365,6 @@ function clearSeedWordCache () { } } -function confirmSeedWords () { - return async dispatch => { - dispatch(actions.showLoadingIndication()) - const account = await dispatch(clearSeedWordCache()) - return dispatch(setIsRevealingSeedWords(false)) - .then(() => { - dispatch(actions.hideLoadingIndication()) - return account - }) - .catch(() => { - dispatch(actions.hideLoadingIndication()) - return account - }) - } -} - function createNewVaultAndRestore (password, seed) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -441,6 +427,30 @@ function revealSeedConfirmation () { } } +function verifyPassword (password) { + return new Promise((resolve, reject) => { + background.submitPassword(password, error => { + if (error) { + return reject(error) + } + + resolve(true) + }) + }) +} + +function verifySeedPhrase () { + return new Promise((resolve, reject) => { + background.verifySeedPhrase((error, seedWords) => { + if (error) { + return reject(error) + } + + resolve(seedWords) + }) + }) +} + function requestRevealSeed (password) { return dispatch => { dispatch(actions.showLoadingIndication()) @@ -460,13 +470,29 @@ function requestRevealSeed (password) { } dispatch(actions.showNewVaultSeed(result)) + dispatch(actions.hideLoadingIndication()) resolve() }) }) }) - .then(() => dispatch(setIsRevealingSeedWords(true))) - .then(() => dispatch(actions.hideLoadingIndication())) - .catch(() => dispatch(actions.hideLoadingIndication())) + } +} + +function requestRevealSeedWords (password) { + return async dispatch => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + + try { + await verifyPassword(password) + const seedWords = await verifySeedPhrase() + dispatch(actions.hideLoadingIndication()) + return seedWords + } catch (error) { + dispatch(actions.hideLoadingIndication()) + dispatch(actions.displayWarning(error.message)) + throw new Error(error.message) + } } } @@ -1923,11 +1949,3 @@ function updateNetworkEndpointType (networkEndpointType) { value: networkEndpointType, } } - -function setIsRevealingSeedWords (reveal) { - return dispatch => { - log.debug(`background.setIsRevealingSeedWords`) - background.setIsRevealingSeedWords(reveal) - return forceUpdateMetamaskState(dispatch) - } -} diff --git a/ui/app/app.js b/ui/app/app.js index 0b38b1326..5af63dc9c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -24,7 +24,7 @@ const Initialized = require('./components/pages/initialized') const Settings = require('./components/pages/settings') const UnlockPage = require('./components/pages/unlock') const RestoreVaultPage = require('./components/pages/keychains/restore-vault') -const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') +const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') const CreateAccountPage = require('./components/pages/create-account') const NoticeScreen = require('./components/pages/notice') @@ -56,20 +56,11 @@ const { class App extends Component { componentWillMount () { - const { - currentCurrency, - setCurrentCurrencyToUSD, - isRevealingSeedWords, - clearSeedWords, - } = this.props + const { currentCurrency, setCurrentCurrencyToUSD } = this.props if (!currentCurrency) { setCurrentCurrencyToUSD() } - - if (isRevealingSeedWords) { - clearSeedWords() - } } renderRoutes () { @@ -402,8 +393,6 @@ App.propTypes = { isMouseUser: PropTypes.bool, setMouseUserState: PropTypes.func, t: PropTypes.func, - isRevealingSeedWords: PropTypes.bool, - clearSeedWords: PropTypes.func, } function mapStateToProps (state) { @@ -484,7 +473,6 @@ function mapDispatchToProps (dispatch, ownProps) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), - clearSeedWords: () => dispatch(actions.confirmSeedWords()), } } diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 4c693d1c3..1ff8eea87 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -280,8 +280,7 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasPrice, min: forceGasMin || MIN_GAS_PRICE_GWEI, - // max: 1000, - step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10), + step: 1, onChange: value => this.convertAndSetGasPrice(value), title: this.context.t('gasPrice'), copy: this.context.t('gasPriceCalculation'), @@ -290,7 +289,6 @@ CustomizeGasModal.prototype.render = function () { h(GasModalCard, { value: convertedGasLimit, min: 1, - // max: 100000, step: 1, onChange: value => this.convertAndSetGasLimit(value), title: this.context.t('gasLimit'), diff --git a/ui/app/components/export-text-container/export-text-container.component.js b/ui/app/components/export-text-container/export-text-container.component.js new file mode 100644 index 000000000..c2546fa9b --- /dev/null +++ b/ui/app/components/export-text-container/export-text-container.component.js @@ -0,0 +1,45 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const copyToClipboard = require('copy-to-clipboard') +const { exportAsFile } = require('../../util') + +class ExportTextContainer extends Component { + render () { + const { text = '', filename = '' } = this.props + const { t } = this.context + + return ( + h('.export-text-container', [ + h('.export-text-container__text-container', [ + h('.export-text-container__text', text), + ]), + h('.export-text-container__buttons-container', [ + h('.export-text-container__button.export-text-container__button--copy', { + onClick: () => copyToClipboard(text), + }, [ + h('img', { src: 'images/copy-to-clipboard.svg' }), + h('.export-text-container__button-text', t('copyToClipboard')), + ]), + h('.export-text-container__button', { + onClick: () => exportAsFile(filename, text), + }, [ + h('img', { src: 'images/download.svg' }), + h('.export-text-container__button-text', t('saveAsCsvFile')), + ]), + ]), + ]) + ) + } +} + +ExportTextContainer.propTypes = { + text: PropTypes.string, + filename: PropTypes.string, +} + +ExportTextContainer.contextTypes = { + t: PropTypes.func, +} + +module.exports = ExportTextContainer diff --git a/ui/app/components/export-text-container/export-text-container.scss b/ui/app/components/export-text-container/export-text-container.scss new file mode 100644 index 000000000..a42de8233 --- /dev/null +++ b/ui/app/components/export-text-container/export-text-container.scss @@ -0,0 +1,52 @@ +.export-text-container { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + border: 1px solid $alto; + border-radius: 4px; + font-weight: 400; + + &__text-container { + width: 100%; + display: flex; + justify-content: center; + padding: 20px; + border-radius: 4px; + background: $alabaster; + } + + &__text { + resize: none; + border: none; + background: $alabaster; + font-size: 20px; + text-align: center; + } + + &__buttons-container { + display: flex; + flex-direction: row; + border-top: 1px solid $alto; + width: 100%; + } + + &__button { + padding: 10px; + flex: 1; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + cursor: pointer; + color: $curious-blue; + + &--copy { + border-right: 1px solid $alto; + } + } + + &__button-text { + padding-left: 10px; + } +} diff --git a/ui/app/components/export-text-container/index.js b/ui/app/components/export-text-container/index.js new file mode 100644 index 000000000..b2864a717 --- /dev/null +++ b/ui/app/components/export-text-container/index.js @@ -0,0 +1,2 @@ +const ExportTextContainer = require('./export-text-container.component') +module.exports = ExportTextContainer diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js index 566e42450..8d52571d0 100644 --- a/ui/app/components/pages/add-token.js +++ b/ui/app/components/pages/add-token.js @@ -192,7 +192,7 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) if (symbol && decimals) { this.setState({ customSymbol: symbol, - customDecimals: decimals.toString(), + customDecimals: decimals, autoFilled: true, }) } diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js index 90b8e1d37..9110f8202 100644 --- a/ui/app/components/pages/home.js +++ b/ui/app/components/pages/home.js @@ -21,7 +21,7 @@ const QrView = require('../../components/qr-code') // Routes const { - REVEAL_SEED_ROUTE, + INITIALIZE_BACKUP_PHRASE_ROUTE, RESTORE_VAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE, NOTICE_ROUTE, @@ -69,7 +69,7 @@ class Home extends Component { log.debug('rendering seed words') return h(Redirect, { to: { - pathname: REVEAL_SEED_ROUTE, + pathname: INITIALIZE_BACKUP_PHRASE_ROUTE, }, }) } diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js index 247f3c8e2..685c81074 100644 --- a/ui/app/components/pages/keychains/reveal-seed.js +++ b/ui/app/components/pages/keychains/reveal-seed.js @@ -2,11 +2,27 @@ const { Component } = require('react') const { connect } = require('react-redux') const PropTypes = require('prop-types') const h = require('react-hyperscript') -const { exportAsFile } = require('../../../util') -const { requestRevealSeed, confirmSeedWords } = require('../../../actions') +const classnames = require('classnames') + +const { requestRevealSeedWords } = require('../../../actions') const { DEFAULT_ROUTE } = require('../../../routes') +const ExportTextContainer = require('../../export-text-container') + +const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN' +const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN' class RevealSeedPage extends Component { + constructor (props) { + super(props) + + this.state = { + screen: PASSWORD_PROMPT_SCREEN, + password: '', + seedWords: null, + error: null, + } + } + componentDidMount () { const passwordBox = document.getElementById('password-box') if (passwordBox) { @@ -14,182 +30,135 @@ class RevealSeedPage extends Component { } } - checkConfirmation (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } + handleSubmit (event) { + event.preventDefault() + this.setState({ seedWords: null, error: null }) + this.props.requestRevealSeedWords(this.state.password) + .then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN })) + .catch(error => this.setState({ error: error.message })) } - revealSeedWords () { - const password = document.getElementById('password-box').value - this.props.requestRevealSeed(password) - } - - renderSeed () { - const { seedWords, confirmSeedWords, history } = this.props - + renderWarning () { return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginTop: 36, - marginBottom: 8, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Vault Created', - ]), - - h('div', { - style: { - fontSize: '1em', - marginTop: '10px', - textAlign: 'center', - }, - }, [ - h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), - ]), - - h('textarea.twelve-word-phrase', { - readOnly: true, - value: seedWords, + h('.page-container__warning-container', [ + h('img.page-container__warning-icon', { + src: 'images/warning.svg', }), - - h('button.primary', { - onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)), - style: { - margin: '24px', - fontSize: '0.9em', - marginBottom: '10px', - }, - }, 'I\'ve copied it somewhere safe'), - - h('button.primary', { - onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords), - style: { - margin: '10px', - fontSize: '0.9em', - }, - }, 'Save Seed Words As File'), + h('.page-container__warning-message', [ + h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]), + h('div', [this.context.t('revealSeedWordsWarning')]), + ]), ]) ) } - renderConfirmation () { - const { history, warning, inProgress } = this.props + renderContent () { + return this.state.screen === PASSWORD_PROMPT_SCREEN + ? this.renderPasswordPromptContent() + : this.renderRevealSeedContent() + } + + renderPasswordPromptContent () { + const { t } = this.context return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', { - style: { maxWidth: '420px' }, + h('form', { + onSubmit: event => this.handleSubmit(event), }, [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), - - // confirmation - h('input.large-input.letter-spacey', { + h('label.input-label', { + htmlFor: 'password-box', + }, t('enterPasswordContinue')), + h('.input-group', [ + h('input.form-control', { type: 'password', + placeholder: t('password'), id: 'password-box', - placeholder: 'Enter your password to confirm', - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, + value: this.state.password, + onChange: event => this.setState({ password: event.target.value }), + className: classnames({ 'form-control--error': this.state.error }), }), - - h('.flex-row.flex-start', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: () => history.push(DEFAULT_ROUTE), - }, 'CANCEL'), - - // submit - h('button.primary', { - style: { marginLeft: '10px' }, - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - warning && ( - h('span.error', { - style: { - margin: '20px', - }, - }, warning.split('-')) - ), - - inProgress && ( - h('span.in-progress-notification', 'Generating Seed...') - ), ]), + this.state.error && h('.reveal-seed__error', this.state.error), + ]) + ) + } + + renderRevealSeedContent () { + const { t } = this.context + + return ( + h('div', [ + h('label.reveal-seed__label', t('yourPrivateSeedPhrase')), + h(ExportTextContainer, { + text: this.state.seedWords, + filename: t('metamaskSeedWords'), + }), + ]) + ) + } + + renderFooter () { + return this.state.screen === PASSWORD_PROMPT_SCREEN + ? this.renderPasswordPromptFooter() + : this.renderRevealSeedFooter() + } + + renderPasswordPromptFooter () { + return ( + h('.page-container__footer', [ + h('button.btn-secondary--lg.page-container__footer-button', { + onClick: () => this.props.history.push(DEFAULT_ROUTE), + }, this.context.t('cancel')), + h('button.btn-primary--lg.page-container__footer-button', { + onClick: event => this.handleSubmit(event), + disabled: this.state.password === '', + }, this.context.t('next')), + ]) + ) + } + + renderRevealSeedFooter () { + return ( + h('.page-container__footer', [ + h('button.btn-secondary--lg.page-container__footer-button', { + onClick: () => this.props.history.push(DEFAULT_ROUTE), + }, this.context.t('close')), ]) ) } render () { - return this.props.seedWords - ? this.renderSeed() - : this.renderConfirmation() + return ( + h('.page-container', [ + h('.page-container__header', [ + h('.page-container__title', this.context.t('revealSeedWordsTitle')), + h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')), + ]), + h('.page-container__content', [ + this.renderWarning(), + h('.reveal-seed__content', [ + this.renderContent(), + ]), + ]), + this.renderFooter(), + ]) + ) } } RevealSeedPage.propTypes = { - requestRevealSeed: PropTypes.func, - confirmSeedWords: PropTypes.func, - seedWords: PropTypes.string, - inProgress: PropTypes.bool, + requestRevealSeedWords: PropTypes.func, history: PropTypes.object, - warning: PropTypes.string, } -const mapStateToProps = state => { - const { appState: { warning }, metamask: { seedWords } } = state - - return { - warning, - seedWords, - } +RevealSeedPage.contextTypes = { + t: PropTypes.func, } const mapDispatchToProps = dispatch => { return { - requestRevealSeed: password => dispatch(requestRevealSeed(password)), - confirmSeedWords: () => dispatch(confirmSeedWords()), + requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)), } } -module.exports = connect(mapStateToProps, mapDispatchToProps)(RevealSeedPage) +module.exports = connect(null, mapDispatchToProps)(RevealSeedPage) diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js index 6ee83ba7e..fb409cb92 100644 --- a/ui/app/components/pending-tx/index.js +++ b/ui/app/components/pending-tx/index.js @@ -8,7 +8,7 @@ const abiDecoder = require('abi-decoder') abiDecoder.addABI(abi) const inherits = require('util').inherits const actions = require('../../actions') -const util = require('../../util') +const { getSymbolAndDecimals } = require('../../token-util') const ConfirmSendEther = require('./confirm-send-ether') const ConfirmSendToken = require('./confirm-send-token') const ConfirmDeployContract = require('./confirm-deploy-contract') @@ -26,6 +26,7 @@ function mapStateToProps (state) { const { conversionRate, identities, + tokens: existingTokens, } = state.metamask const accounts = state.metamask.accounts const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] @@ -33,6 +34,7 @@ function mapStateToProps (state) { conversionRate, identities, selectedAddress, + existingTokens, } } @@ -66,6 +68,7 @@ PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) { } PendingTx.prototype.setTokenData = async function () { + const { existingTokens } = this.props const txMeta = this.gatherTxMeta() const txParams = txMeta.txParams || {} @@ -89,30 +92,15 @@ PendingTx.prototype.setTokenData = async function () { } if (isTokenTransaction) { - const token = util.getContractAtAddress(txParams.to) - const results = await Promise.all([ - token.symbol(), - token.decimals(), - ]) - const [ symbol, decimals ] = results + const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens) - if (symbol[0] && decimals[0]) { - this.setState({ - transactionType: TX_TYPES.SEND_TOKEN, - tokenAddress: txParams.to, - tokenSymbol: symbol[0], - tokenDecimals: decimals[0], - isFetching: false, - }) - } else { - this.setState({ - transactionType: TX_TYPES.SEND_TOKEN, - tokenAddress: txParams.to, - tokenSymbol: null, - tokenDecimals: null, - isFetching: false, - }) - } + this.setState({ + transactionType: TX_TYPES.SEND_TOKEN, + tokenAddress: txParams.to, + tokenSymbol: symbol, + tokenDecimals: decimals, + isFetching: false, + }) } else { this.setState({ transactionType: TX_TYPES.SEND_ETHER, diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index a7bd5d7ea..90fb2b66c 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -89,7 +89,6 @@ CurrencyDisplay.prototype.render = function () { } = this.props const valueToRender = this.getValueToRender() - const convertedValueToRender = this.getConvertedValueToRender(valueToRender) return h('div', { @@ -97,22 +96,24 @@ CurrencyDisplay.prototype.render = function () { style: { borderColor: inError ? 'red' : null, }, - onClick: () => this.currencyInput.focus(), + onClick: () => this.currencyInput && this.currencyInput.focus(), }, [ h('div.currency-display__primary-row', [ h('div.currency-display__input-wrapper', [ - h(CurrencyInput, { + h(readOnly ? 'input' : CurrencyInput, { className: primaryBalanceClassName, value: `${valueToRender}`, placeholder: '0', readOnly, - onInputChange: newValue => { - handleChange(this.getAmount(newValue)) - }, - inputRef: input => { this.currencyInput = input }, + ...(!readOnly ? { + onInputChange: newValue => { + handleChange(this.getAmount(newValue)) + }, + inputRef: input => { this.currencyInput = input }, + } : {}), }), h('span.currency-display__currency-symbol', primaryCurrency), diff --git a/ui/app/components/send/send-constants.js b/ui/app/components/send/send-constants.js index b3ee0899a..5d89c74aa 100644 --- a/ui/app/components/send/send-constants.js +++ b/ui/app/components/send/send-constants.js @@ -1,8 +1,8 @@ const ethUtil = require('ethereumjs-util') const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') -const MIN_GAS_PRICE_HEX = (100000000).toString(16) -const MIN_GAS_PRICE_DEC = '100000000' +const MIN_GAS_PRICE_DEC = '0' +const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16) const MIN_GAS_LIMIT_DEC = '21000' const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16) diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index fd4a80a4a..22ab64426 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -55,6 +55,10 @@ function ShapeshiftForm () { } } +ShapeshiftForm.prototype.getCoinPair = function () { + return `${this.state.depositCoin.toUpperCase()}_ETH` +} + ShapeshiftForm.prototype.componentWillMount = function () { this.props.shapeShiftSubview() } @@ -120,14 +124,12 @@ ShapeshiftForm.prototype.renderMetadata = function (label, value) { } ShapeshiftForm.prototype.renderMarketInfo = function () { - const { depositCoin } = this.state - const coinPair = `${depositCoin}_eth` const { tokenExchangeRates } = this.props const { limit, rate, minimum, - } = tokenExchangeRates[coinPair] || {} + } = tokenExchangeRates[this.getCoinPair()] || {} return h('div.shapeshift-form__metadata', {}, [ @@ -172,10 +174,9 @@ ShapeshiftForm.prototype.renderQrCode = function () { ShapeshiftForm.prototype.render = function () { const { coinOptions, btnClass, warning } = this.props - const { depositCoin, errorMessage, showQrCode, depositAddress } = this.state - const coinPair = `${depositCoin}_eth` + const { errorMessage, showQrCode, depositAddress } = this.state const { tokenExchangeRates } = this.props - const token = tokenExchangeRates[coinPair] + const token = tokenExchangeRates[this.getCoinPair()] return h('div.shapeshift-form-wrapper', [ showQrCode diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 959eb9d15..1c544e162 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -61,3 +61,5 @@ @import './welcome-screen.scss'; @import './sender-to-recipient.scss'; + +@import '../../../components/export-text-container/export-text-container.scss'; diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss index 82446fd7a..d0b59da53 100644 --- a/ui/app/css/itcss/components/pages/index.scss +++ b/ui/app/css/itcss/components/pages/index.scss @@ -1 +1,3 @@ @import './unlock.scss'; + +@import './reveal-seed.scss'; diff --git a/ui/app/css/itcss/components/pages/reveal-seed.scss b/ui/app/css/itcss/components/pages/reveal-seed.scss new file mode 100644 index 000000000..b8f13af4a --- /dev/null +++ b/ui/app/css/itcss/components/pages/reveal-seed.scss @@ -0,0 +1,17 @@ +.reveal-seed { + &__content { + padding: 20px; + } + + &__label { + padding-bottom: 10px; + font-weight: 400; + display: inline-block; + } + + &__error { + color: $crimson; + font-size: 14px; + padding-top: 5px; + } +} diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss index 92321394b..7a64810c4 100644 --- a/ui/app/css/itcss/generic/index.scss +++ b/ui/app/css/itcss/generic/index.scss @@ -207,6 +207,27 @@ input.large-input { &__content { height: 100%; overflow-y: auto; + min-height: 250px; + max-height: 400px; + } + + &__warning-container { + background: $linen; + padding: 20px; + display: flex; + align-items: start; + } + + &__warning-message { + padding-left: 15px; + } + + &__warning-title { + font-weight: 500; + } + + &__warning-icon { + padding-top: 5px; } } @@ -237,3 +258,49 @@ input.large-input { border-radius: 0; } } + +@media screen and (min-width: 576px) { + .page-container { + height: 600px; + flex: 0 0 auto; + } +} + +.input-label { + padding-bottom: 10px; + font-weight: 400; + display: inline-block; +} + +input.form-control { + padding-left: 10px; + font-size: 14px; + height: 40px; + border: 1px solid $alto; + border-radius: 3px; + width: 100%; + + &::-webkit-input-placeholder { + font-weight: 100; + color: $dusty-gray; + } + + &::-moz-placeholder { + font-weight: 100; + color: $dusty-gray; + } + + &:-ms-input-placeholder { + font-weight: 100; + color: $dusty-gray; + } + + &:-moz-placeholder { + font-weight: 100; + color: $dusty-gray; + } + + &--error { + border: 1px solid $monzo; + } +} diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 51548306f..814d7a382 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -54,6 +54,7 @@ $saffron: #f6c343; $dodger-blue: #3099f2; $zumthor: #edf7ff; $ecstasy: #f7861c; +$linen: #fdf4f4; /* Z-Indicies diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/app/keychains/hd/recover-seed/confirmation.js deleted file mode 100644 index eb588415f..000000000 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ /dev/null @@ -1,138 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const actions = require('../../../actions') -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const { - DEFAULT_ROUTE, - INITIALIZE_BACKUP_PHRASE_ROUTE, -} = require('../../../routes') - -RevealSeedConfirmation.contextTypes = { - t: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps) -)(RevealSeedConfirmation) - - -inherits(RevealSeedConfirmation, Component) -function RevealSeedConfirmation () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -RevealSeedConfirmation.prototype.render = function () { - const props = this.props - - return ( - - h('.initialize-screen.flex-column.flex-center.flex-grow', { - style: { maxWidth: '420px' }, - }, [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - 'Reveal Seed Words', - ]), - - h('.div', { - style: { - display: 'flex', - flexDirection: 'column', - padding: '20px', - justifyContent: 'center', - }, - }, [ - - h('h4', this.context.t('revealSeedWordsWarning')), - - // confirmation - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: this.context.t('enterPasswordConfirm'), - onKeyPress: this.checkConfirmation.bind(this), - style: { - width: 260, - marginTop: '12px', - }, - }), - - h('.flex-row.flex-start', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - // cancel - h('button.primary', { - onClick: this.goHome.bind(this), - }, 'CANCEL'), - - // submit - h('button.primary', { - style: { marginLeft: '10px' }, - onClick: this.revealSeedWords.bind(this), - }, 'OK'), - - ]), - - (props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, props.warning.split('-')) - ), - - props.inProgress && ( - h('span.in-progress-notification', this.context.t('generatingSeed')) - ), - ]), - ]) - ) -} - -RevealSeedConfirmation.prototype.componentDidMount = function () { - document.getElementById('password-box').focus() -} - -RevealSeedConfirmation.prototype.goHome = function () { - this.props.dispatch(actions.showConfigPage(false)) - this.props.dispatch(actions.confirmSeedWords()) - .then(() => this.props.history.push(DEFAULT_ROUTE)) -} - -// create vault - -RevealSeedConfirmation.prototype.checkConfirmation = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.revealSeedWords() - } -} - -RevealSeedConfirmation.prototype.revealSeedWords = function () { - var password = document.getElementById('password-box').value - this.props.dispatch(actions.requestRevealSeed(password)) - .then(() => this.props.history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)) -} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 30d3d3152..bd00b186e 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -493,7 +493,7 @@ SendTransactionScreen.prototype.renderFooter = function () { history, } = this.props - const missingTokenBalance = selectedToken && !tokenBalance + const missingTokenBalance = selectedToken && (tokenBalance === null || tokenBalance === undefined) const noErrors = !amountError && toError === null return h('div.page-container__footer', [ diff --git a/ui/app/token-util.js b/ui/app/token-util.js index f84051ef5..920442bfc 100644 --- a/ui/app/token-util.js +++ b/ui/app/token-util.js @@ -1,14 +1,6 @@ -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') - -const tokenInfoGetter = function () { - if (typeof global.ethereumProvider === 'undefined') return - - const eth = new Eth(global.ethereumProvider) - const contract = new EthContract(eth) - const TokenContract = contract(abi) +const util = require('./util') +function tokenInfoGetter () { const tokens = {} return async (address) => { @@ -16,21 +8,38 @@ const tokenInfoGetter = function () { return tokens[address] } - const contract = TokenContract.at(address) - - const result = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol = [], decimals = [] ] = result - - tokens[address] = { symbol: symbol[0], decimals: decimals[0] } + tokens[address] = await getSymbolAndDecimals(address) return tokens[address] } } +async function getSymbolAndDecimals (tokenAddress, existingTokens = []) { + const existingToken = existingTokens.find(({ address }) => tokenAddress === address) + if (existingToken) { + return existingToken + } + + let result = [] + try { + const token = util.getContractAtAddress(tokenAddress) + + result = await Promise.all([ + token.symbol(), + token.decimals(), + ]) + } catch (err) { + console.log(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, err) + } + + const [ symbol = [], decimals = [] ] = result + + return { + symbol: symbol[0] || null, + decimals: decimals[0] && decimals[0].toString() || null, + } +} + function calcTokenAmount (value, decimals) { const multiplier = Math.pow(10, Number(decimals || 0)) const amount = Number(value / multiplier) @@ -42,4 +51,5 @@ function calcTokenAmount (value, decimals) { module.exports = { tokenInfoGetter, calcTokenAmount, + getSymbolAndDecimals, }