From ab0eae1ed3f3890eb564a770dcae930b42a2691f Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 30 Oct 2019 16:18:57 -0300 Subject: [PATCH 01/28] Rename ConfirmPageContainerHeader class (#7322) The class has been renamed to reflect that it is a header, to avoid having the same name as the `ConfirmPageContainer` component. Multiple components with the same name can lead to confusing error messages. --- .../confirm-page-container-header.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index 84ca40da5..4314d21eb 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -6,7 +6,7 @@ import { } from '../../../../../../app/scripts/lib/enums' import NetworkDisplay from '../../network-display' -export default class ConfirmPageContainer extends Component { +export default class ConfirmPageContainerHeader extends Component { static contextTypes = { t: PropTypes.func, } From 51e5220d5eaa08f5a39100cdf1e9c31f55a91969 Mon Sep 17 00:00:00 2001 From: Frankie Date: Wed, 30 Oct 2019 11:40:33 -1000 Subject: [PATCH 02/28] I#3669 ignore known transactions on first broadcast and continue with normal flow (#7328) * transactions - ignore known tx errors * tests - test ignoreing Transaction Failed: known transaction message --- app/scripts/controllers/transactions/index.js | 13 ++++++++++++- .../controllers/transactions/tx-controller-test.js | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 22ea58142..408bed283 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -439,8 +439,19 @@ class TransactionController extends EventEmitter { const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction') - const txHash = await this.query.sendRawTransaction(rawTx) + let txHash + try { + txHash = await this.query.sendRawTransaction(rawTx) + } catch (error) { + if (error.message.toLowerCase().includes('known transaction')) { + txHash = ethUtil.sha3(ethUtil.addHexPrefix(rawTx)).toString('hex') + txHash = ethUtil.addHexPrefix(txHash) + } else { + throw error + } + } this.setTxHash(txId, txHash) + this.txStateManager.setTxStatusSubmitted(txId) } diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 642e1b6af..76b8e5025 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -496,6 +496,16 @@ describe('Transaction Controller', function () { assert.equal(publishedTx.hash, hash) assert.equal(publishedTx.status, 'submitted') }) + + it('should ignore the error "Transaction Failed: known transaction" and be as usual', async function () { + providerResultStub['eth_sendRawTransaction'] = async (_, __, ___, end) => { end('Transaction Failed: known transaction') } + const rawTx = '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a' + txController.txStateManager.addTx(txMeta) + await txController.publishTransaction(txMeta.id, rawTx) + const publishedTx = txController.txStateManager.getTx(1) + assert.equal(publishedTx.hash, '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09') + assert.equal(publishedTx.status, 'submitted') + }) }) describe('#retryTransaction', function () { From 514be408f8eef60b27b79743d6525bf60a354ce4 Mon Sep 17 00:00:00 2001 From: Frankie Date: Wed, 30 Oct 2019 12:15:54 -1000 Subject: [PATCH 03/28] I#6704 eth_getTransactionByHash will now check metamask's local history for pending transactions (#7327) * tests - create tests for pending middlewares * transactions - add r,s,v values to the txMeta to match the JSON rpc response * network - add new middleware for eth_getTransactionByHash that the checks pending tx's for a response value * transactions/pending - use getTransactionReceipt for checking if tx is in a block * meta - file rename --- .../network/createMetamaskMiddleware.js | 15 +- .../controllers/network/middleware/pending.js | 28 +++ app/scripts/controllers/network/util.js | 21 ++ app/scripts/controllers/transactions/index.js | 9 + .../transactions/pending-tx-tracker.js | 4 +- app/scripts/metamask-controller.js | 1 + .../network-controller-test.js} | 4 +- .../network/pending-middleware-test.js | 81 +++++++ test/unit/app/controllers/network/stubs.js | 225 ++++++++++++++++++ 9 files changed, 372 insertions(+), 16 deletions(-) create mode 100644 app/scripts/controllers/network/middleware/pending.js rename test/unit/app/controllers/{network-contoller-test.js => network/network-controller-test.js} (95%) create mode 100644 test/unit/app/controllers/network/pending-middleware-test.js create mode 100644 test/unit/app/controllers/network/stubs.js diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index 5dcd3a895..58ccb95a1 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -1,8 +1,7 @@ const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') const createWalletSubprovider = require('eth-json-rpc-middleware/wallet') - +const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('./middleware/pending') module.exports = createMetamaskMiddleware function createMetamaskMiddleware ({ @@ -15,6 +14,7 @@ function createMetamaskMiddleware ({ processTypedMessageV4, processPersonalMessage, getPendingNonce, + getPendingTransactionByHash, }) { const metamaskMiddleware = mergeMiddleware([ createScaffoldMiddleware({ @@ -32,16 +32,7 @@ function createMetamaskMiddleware ({ processPersonalMessage, }), createPendingNonceMiddleware({ getPendingNonce }), + createPendingTxMiddleware({ getPendingTransactionByHash }), ]) return metamaskMiddleware } - -function createPendingNonceMiddleware ({ getPendingNonce }) { - return createAsyncMiddleware(async (req, res, next) => { - if (req.method !== 'eth_getTransactionCount') return next() - const address = req.params[0] - const blockRef = req.params[1] - if (blockRef !== 'pending') return next() - res.result = await getPendingNonce(address) - }) -} diff --git a/app/scripts/controllers/network/middleware/pending.js b/app/scripts/controllers/network/middleware/pending.js new file mode 100644 index 000000000..542d8bde6 --- /dev/null +++ b/app/scripts/controllers/network/middleware/pending.js @@ -0,0 +1,28 @@ +const { formatTxMetaForRpcResult } = require('../util') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') + +function createPendingNonceMiddleware ({ getPendingNonce }) { + return createAsyncMiddleware(async (req, res, next) => { + const {method, params} = req + if (method !== 'eth_getTransactionCount') return next() + const [param, blockRef] = params + if (blockRef !== 'pending') return next() + res.result = await getPendingNonce(param) + }) +} + +function createPendingTxMiddleware ({ getPendingTransactionByHash }) { + return createAsyncMiddleware(async (req, res, next) => { + const {method, params} = req + if (method !== 'eth_getTransactionByHash') return next() + const [hash] = params + const txMeta = getPendingTransactionByHash(hash) + if (!txMeta) return next() + res.result = formatTxMetaForRpcResult(txMeta) + }) +} + +module.exports = { + createPendingTxMiddleware, + createPendingNonceMiddleware, +} diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js index a6f848864..829c62582 100644 --- a/app/scripts/controllers/network/util.js +++ b/app/scripts/controllers/network/util.js @@ -29,6 +29,27 @@ const networkToNameMap = { const getNetworkDisplayName = key => networkToNameMap[key] +function formatTxMetaForRpcResult (txMeta) { + return { + 'blockHash': txMeta.txReceipt ? txMeta.txReceipt.blockHash : null, + 'blockNumber': txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null, + 'from': txMeta.txParams.from, + 'gas': txMeta.txParams.gas, + 'gasPrice': txMeta.txParams.gasPrice, + 'hash': txMeta.hash, + 'input': txMeta.txParams.data || '0x', + 'nonce': txMeta.txParams.nonce, + 'to': txMeta.txParams.to, + 'transactionIndex': txMeta.txReceipt ? txMeta.txReceipt.transactionIndex : null, + 'value': txMeta.txParams.value || '0x0', + 'v': txMeta.v, + 'r': txMeta.r, + 's': txMeta.s, + } +} + + module.exports = { getNetworkDisplayName, + formatTxMetaForRpcResult, } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 408bed283..df9fb6502 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -423,6 +423,15 @@ class TransactionController extends EventEmitter { const fromAddress = txParams.from const ethTx = new Transaction(txParams) await this.signEthTx(ethTx, fromAddress) + + // add r,s,v values for provider request purposes see createMetamaskMiddleware + // and JSON rpc standard for further explanation + txMeta.r = ethUtil.bufferToHex(ethTx.r) + txMeta.s = ethUtil.bufferToHex(ethTx.s) + txMeta.v = ethUtil.bufferToHex(ethTx.v) + + this.txStateManager.updateTx(txMeta, 'transactions#signTransaction: add r, s, v values') + // set state to signed this.txStateManager.setTxStatusSigned(txMeta.id) const rawTx = ethUtil.bufferToHex(ethTx.serialize()) diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 1ef3be36e..8f4076f45 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -174,7 +174,7 @@ class PendingTransactionTracker extends EventEmitter { // get latest transaction status try { - const { blockNumber } = await this.query.getTransactionByHash(txHash) || {} + const { blockNumber } = await this.query.getTransactionReceipt(txHash) || {} if (blockNumber) { this.emit('tx:confirmed', txId) } @@ -196,7 +196,7 @@ class PendingTransactionTracker extends EventEmitter { async _checkIftxWasDropped (txMeta) { const { txParams: { nonce, from }, hash } = txMeta const nextNonce = await this.query.getTransactionCount(from) - const { blockNumber } = await this.query.getTransactionByHash(hash) || {} + const { blockNumber } = await this.query.getTransactionReceipt(hash) || {} if (!blockNumber && parseInt(nextNonce) > parseInt(nonce)) { return true } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e3f332d4c..eac6d1e81 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -353,6 +353,7 @@ module.exports = class MetamaskController extends EventEmitter { processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), getPendingNonce: this.getPendingNonce.bind(this), + getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0], } const providerProxy = this.networkController.initializeProvider(providerOpts) return providerProxy diff --git a/test/unit/app/controllers/network-contoller-test.js b/test/unit/app/controllers/network/network-controller-test.js similarity index 95% rename from test/unit/app/controllers/network-contoller-test.js rename to test/unit/app/controllers/network/network-controller-test.js index 32f7b337d..b63a23a4f 100644 --- a/test/unit/app/controllers/network-contoller-test.js +++ b/test/unit/app/controllers/network/network-controller-test.js @@ -1,9 +1,9 @@ const assert = require('assert') const nock = require('nock') -const NetworkController = require('../../../../app/scripts/controllers/network') +const NetworkController = require('../../../../../app/scripts/controllers/network') const { getNetworkDisplayName, -} = require('../../../../app/scripts/controllers/network/util') +} = require('../../../../../app/scripts/controllers/network/util') describe('# Network Controller', function () { let networkController diff --git a/test/unit/app/controllers/network/pending-middleware-test.js b/test/unit/app/controllers/network/pending-middleware-test.js new file mode 100644 index 000000000..838395b0b --- /dev/null +++ b/test/unit/app/controllers/network/pending-middleware-test.js @@ -0,0 +1,81 @@ +const assert = require('assert') +const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('../../../../../app/scripts/controllers/network/middleware/pending') +const txMetaStub = require('./stubs').txMetaStub +describe('#createPendingNonceMiddleware', function () { + const getPendingNonce = async () => '0x2' + const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748' + const pendingNonceMiddleware = createPendingNonceMiddleware({ getPendingNonce }) + + it('should call next if not a eth_getTransactionCount request', (done) => { + const req = {method: 'eth_getBlockByNumber'} + const res = {} + pendingNonceMiddleware(req, res, () => done()) + }) + it('should call next if not a "pending" block request', (done) => { + const req = { method: 'eth_getTransactionCount', params: [address] } + const res = {} + pendingNonceMiddleware(req, res, () => done()) + }) + it('should fill the result with a the "pending" nonce', (done) => { + const req = { method: 'eth_getTransactionCount', params: [address, 'pending'] } + const res = {} + pendingNonceMiddleware(req, res, () => { done(new Error('should not have called next')) }, () => { + assert(res.result === '0x2') + done() + }) + }) +}) + +describe('#createPendingTxMiddleware', function () { + let returnUndefined = true + const getPendingTransactionByHash = () => returnUndefined ? undefined : txMetaStub + const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748' + const pendingTxMiddleware = createPendingTxMiddleware({ getPendingTransactionByHash }) + const spec = { + 'blockHash': null, + 'blockNumber': null, + 'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'gas': '0x5208', + 'gasPrice': '0x1e8480', + 'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', + 'input': '0x', + 'nonce': '0x4', + 'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'transactionIndex': null, + 'value': '0x0', + 'v': '0x2c', + 'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', + 's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + } + it('should call next if not a eth_getTransactionByHash request', (done) => { + const req = {method: 'eth_getBlockByNumber'} + const res = {} + pendingTxMiddleware(req, res, () => done()) + }) + + it('should call next if no pending txMeta is in history', (done) => { + const req = { method: 'eth_getTransactionByHash', params: [address] } + const res = {} + pendingTxMiddleware(req, res, () => done()) + }) + + it('should fill the result with a the "pending" tx the result should match the rpc spec', (done) => { + returnUndefined = false + const req = { method: 'eth_getTransactionByHash', params: [address, 'pending'] } + const res = {} + pendingTxMiddleware(req, res, () => { done(new Error('should not have called next')) }, () => { + /* + // uncomment this section for debugging help with non matching keys + const coppy = {...res.result} + Object.keys(spec).forEach((key) => { + console.log(coppy[key], '===', spec[key], coppy[key] === spec[key], key) + delete coppy[key] + }) + console.log(coppy) + */ + assert.deepStrictEqual(res.result, spec, new Error('result does not match the spec object')) + done() + }) + }) + +}) diff --git a/test/unit/app/controllers/network/stubs.js b/test/unit/app/controllers/network/stubs.js new file mode 100644 index 000000000..1551cd581 --- /dev/null +++ b/test/unit/app/controllers/network/stubs.js @@ -0,0 +1,225 @@ +/* + this file is for all my big stubs because i don't want to + to mingle with my tests +*/ + +module.exports = {} + +// for pending middlewares test +module.exports.txMetaStub = { + 'estimatedGas': '0x5208', + 'firstRetryBlockNumber': '0x51a402', + 'gasLimitSpecified': true, + 'gasPriceSpecified': true, + 'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', + 'history': [ + { + 'id': 405984854664302, + 'loadingDefaults': true, + 'metamaskNetworkId': '4', + 'status': 'unapproved', + 'time': 1572395156620, + 'transactionCategory': 'sentEther', + 'txParams': { + 'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'gas': '0x5208', + 'gasPrice': '0x1e8480', + 'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'value': '0x0', + }, + 'type': 'standard', + }, + [ + { + 'op': 'replace', + 'path': '/loadingDefaults', + 'timestamp': 1572395156645, + 'value': false, + }, + { + 'op': 'add', + 'path': '/gasPriceSpecified', + 'value': true, + }, + { + 'op': 'add', + 'path': '/gasLimitSpecified', + 'value': true, + }, + { + 'op': 'add', + 'path': '/estimatedGas', + 'value': '0x5208', + }, + ], + [ + { + 'note': '#newUnapprovedTransaction - adding the origin', + 'op': 'add', + 'path': '/origin', + 'timestamp': 1572395156645, + 'value': 'MetaMask', + }, + ], + [], + [ + { + 'note': 'txStateManager: setting status to approved', + 'op': 'replace', + 'path': '/status', + 'timestamp': 1572395158240, + 'value': 'approved', + }, + ], + [ + { + 'note': 'transactions#approveTransaction', + 'op': 'add', + 'path': '/txParams/nonce', + 'timestamp': 1572395158261, + 'value': '0x4', + }, + { + 'op': 'add', + 'path': '/nonceDetails', + 'value': { + 'local': { + 'details': { + 'highest': 4, + 'startPoint': 4, + }, + 'name': 'local', + 'nonce': 4, + }, + 'network': { + 'details': { + 'baseCount': 4, + 'blockNumber': '0x51a401', + }, + 'name': 'network', + 'nonce': 4, + }, + 'params': { + 'highestLocallyConfirmed': 0, + 'highestSuggested': 4, + 'nextNetworkNonce': 4, + }, + }, + }, + ], + [ + { + 'note': 'transactions#signTransaction: add r, s, v values', + 'op': 'add', + 'path': '/r', + 'timestamp': 1572395158280, + 'value': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', + }, + { + 'op': 'add', + 'path': '/s', + 'value': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + }, + { + 'op': 'add', + 'path': '/v', + 'value': '0x2c', + }, + ], + [ + { + 'note': 'transactions#publishTransaction', + 'op': 'replace', + 'path': '/status', + 'timestamp': 1572395158281, + 'value': 'signed', + }, + { + 'op': 'add', + 'path': '/rawTx', + 'value': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + }, + ], + [], + [ + { + 'note': 'transactions#setTxHash', + 'op': 'add', + 'path': '/hash', + 'timestamp': 1572395158570, + 'value': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', + }, + ], + [ + { + 'note': 'txStateManager - add submitted time stamp', + 'op': 'add', + 'path': '/submittedTime', + 'timestamp': 1572395158571, + 'value': 1572395158570, + }, + ], + [ + { + 'note': 'txStateManager: setting status to submitted', + 'op': 'replace', + 'path': '/status', + 'timestamp': 1572395158576, + 'value': 'submitted', + }, + ], + [ + { + 'note': 'transactions/pending-tx-tracker#event: tx:block-update', + 'op': 'add', + 'path': '/firstRetryBlockNumber', + 'timestamp': 1572395168972, + 'value': '0x51a402', + }, + ], + ], + 'id': 405984854664302, + 'loadingDefaults': false, + 'metamaskNetworkId': '4', + 'nonceDetails': { + 'local': { + 'details': { + 'highest': 4, + 'startPoint': 4, + }, + 'name': 'local', + 'nonce': 4, + }, + 'network': { + 'details': { + 'baseCount': 4, + 'blockNumber': '0x51a401', + }, + 'name': 'network', + 'nonce': 4, + }, + 'params': { + 'highestLocallyConfirmed': 0, + 'highestSuggested': 4, + 'nextNetworkNonce': 4, + }, + }, + 'origin': 'MetaMask', + 'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', + 'rawTx': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + 's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + 'status': 'submitted', + 'submittedTime': 1572395158570, + 'time': 1572395156620, + 'transactionCategory': 'sentEther', + 'txParams': { + 'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'gas': '0x5208', + 'gasPrice': '0x1e8480', + 'nonce': '0x4', + 'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'value': '0x0', + }, + 'type': 'standard', + 'v': '0x2c', +} From 19965985ad9bbc12853da7f3943b86e37aae0db2 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Wed, 30 Oct 2019 22:31:04 -0300 Subject: [PATCH 04/28] Update `ethereumjs-util` (#7332) `ethereumjs-util` is now pinned at `5.1.0`, instead of at the commit `ac5d0908536b447083ea422b435da27f26615de9`. That commit immediately preceded v5.1.0, so there are no functional differences. This was done mainly to remove our last GitHub/git dependency, and to make it more obvious which version we're using. --- package.json | 2 +- yarn.lock | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 6b8a3d3ad..84a1f0e24 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "eth-trezor-keyring": "^0.4.0", "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "1.3.7", - "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", + "ethereumjs-util": "5.1.0", "ethereumjs-wallet": "^0.6.0", "etherscan-link": "^1.0.2", "ethjs": "^0.4.0", diff --git a/yarn.lock b/yarn.lock index 00e79c3cb..b4753bcf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10126,6 +10126,17 @@ ethereumjs-tx@1.3.7, ethereumjs-tx@^1.1.1, ethereumjs-tx@^1.2.0, ethereumjs-tx@^ ethereum-common "^0.0.18" ethereumjs-util "^5.0.0" +ethereumjs-util@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.1.0.tgz#8e9646c13322e75a9c593cf705d27d4a51c991c0" + integrity sha1-jpZGwTMi51qcWTz3BdJ9SlHJkcA= + dependencies: + bn.js "^4.8.0" + create-hash "^1.1.2" + keccak "^1.0.2" + rlp "^2.0.0" + secp256k1 "^3.0.1" + ethereumjs-util@6.1.0, ethereumjs-util@^6.0.0, ethereumjs-util@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz#e9c51e5549e8ebd757a339cc00f5380507e799c8" @@ -10163,16 +10174,6 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum safe-buffer "^5.1.1" secp256k1 "^3.0.1" -"ethereumjs-util@github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9": - version "5.0.1" - resolved "https://codeload.github.com/ethereumjs/ethereumjs-util/tar.gz/ac5d0908536b447083ea422b435da27f26615de9" - dependencies: - bn.js "^4.8.0" - create-hash "^1.1.2" - keccak "^1.0.2" - rlp "^2.0.0" - secp256k1 "^3.0.1" - ethereumjs-util@~6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.0.0.tgz#f14841c182b918615afefd744207c7932c8536c0" From fe28e0d13427ec5847b2099a946b9388b2ac4cd2 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 31 Oct 2019 13:27:22 -0300 Subject: [PATCH 05/28] Cleanup beforeunload handler after transaction is resolved (#7333) * Cleanup beforeunload handler after transaction is resolved The notification window was updated to reject transactions upon close in #6340. A handler that rejects the transaction was added to `window.onbeforeunload`, and it was cleared in `actions.js` if it was confirmed or rejected. However, the `onbeforeunload` handler remained uncleared if the transaction was resolved in another window. This results in the transaction being rejected when the notification window closes, even long after the transaction is submitted and confirmed. This has been the cause of many problems with the Firefox e2e tests. Instead the `onbeforeunload` handler is cleared in the `componentWillUnmount` lifecycle function, alongside where it's set in the first place. This ensures that it's correctly unset regardless of how the transaction was resolved, and it better matches user expectations. * Fix indentation and remove redundant export The `run-all.sh` Bash script now uses consistent indentation, and is consistent about only re-exporting the Ganache arguments when they change. * Ensure transactions are completed before checking balance Various intermittent e2e test failures appear to be caused by React re-rendering the transaction list during the test, as the transaction goes from pending to confirmed. To avoid this race condition, the transaction is now explicitly looked for in the confirmed transaction list in each of the tests using this pattern. * Enable all e2e tests on Firefox The remaining tests that were disabled on Firefox now work correctly. Only a few timing adjustments were needed. * Update Firefox used in CI Firefox v70 is now used on CI instead of v68. This necessitated rewriting the function where the extension ID was obtained because the Firefox extensions page was redesigned. --- .circleci/scripts/firefox-install | 2 +- test/e2e/address-book.spec.js | 24 ++--- test/e2e/from-import-ui.spec.js | 6 +- test/e2e/func.js | 2 +- test/e2e/metamask-responsive-ui.spec.js | 12 +-- test/e2e/metamask-ui.spec.js | 97 ++++++------------- test/e2e/run-all.sh | 33 +++---- test/e2e/send-edit.spec.js | 6 +- ui/app/components/app/signature-request.js | 9 +- .../confirm-transaction-base.component.js | 9 +- .../import-with-seed-phrase.component.js | 7 +- ui/app/store/actions.js | 12 --- 12 files changed, 96 insertions(+), 123 deletions(-) diff --git a/.circleci/scripts/firefox-install b/.circleci/scripts/firefox-install index 3f0772f49..21766467e 100755 --- a/.circleci/scripts/firefox-install +++ b/.circleci/scripts/firefox-install @@ -4,7 +4,7 @@ set -e set -u set -o pipefail -FIREFOX_VERSION='68.0' +FIREFOX_VERSION='70.0' FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2" FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}" FIREFOX_PATH='/opt/firefox' diff --git a/test/e2e/address-book.spec.js b/test/e2e/address-book.spec.js index cac5ecec2..b4a709825 100644 --- a/test/e2e/address-book.spec.js +++ b/test/e2e/address-book.spec.js @@ -210,13 +210,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -251,13 +251,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 2) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 2 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000) }) }) }) diff --git a/test/e2e/from-import-ui.spec.js b/test/e2e/from-import-ui.spec.js index abb2a5aaa..ce224e781 100644 --- a/test/e2e/from-import-ui.spec.js +++ b/test/e2e/from-import-ui.spec.js @@ -226,8 +226,10 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) assert.equal(txValues.length, 1) diff --git a/test/e2e/func.js b/test/e2e/func.js index dfad8466c..ab94f6231 100644 --- a/test/e2e/func.js +++ b/test/e2e/func.js @@ -91,7 +91,7 @@ async function getExtensionIdChrome (driver) { async function getExtensionIdFirefox (driver) { await driver.get('about:debugging#addons') - const extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText() + const extensionId = await driver.wait(webdriver.until.elementLocated(By.xpath('//dl/div[contains(., \'Internal UUID\')]/dd')), 1000).getText() return extensionId } diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js index 980544eba..90cf35710 100644 --- a/test/e2e/metamask-responsive-ui.spec.js +++ b/test/e2e/metamask-responsive-ui.spec.js @@ -231,13 +231,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) }) diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index db50e3279..868028cd1 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -289,13 +289,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -332,13 +332,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 2) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 2 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -385,13 +385,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 3) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 3 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -838,12 +838,10 @@ describe('MetaMask', function () { it('renders the correct ETH balance', async () => { const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance')) await delay(regularDelayMs) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - await driver.wait(until.elementTextMatches(balance, /^87.*\s*ETH.*$/), 10000) - const tokenAmount = await balance.getText() - assert.ok(/^87.*\s*ETH.*$/.test(tokenAmount)) - await delay(regularDelayMs) - } + await driver.wait(until.elementTextMatches(balance, /^87.*\s*ETH.*$/), 10000) + const tokenAmount = await balance.getText() + assert.ok(/^87.*\s*ETH.*$/.test(tokenAmount)) + await delay(regularDelayMs) }) }) @@ -1002,22 +1000,15 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) - - const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) - assert.equal(txValues.length, 1) - - // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, - // or possibly until we use latest version of firefox in the tests - if (process.env.SELENIUM_BROWSER !== 'firefox') { - await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000) - } - await driver.wait(async () => { const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 }, 10000) + + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + assert.equal(txValues.length, 1) + await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/i), 10000) }) @@ -1104,7 +1095,6 @@ describe('MetaMask', function () { return confirmedTxes.length === 2 }, 10000) - await delay(regularDelayMs) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/)) const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) @@ -1115,14 +1105,10 @@ describe('MetaMask', function () { const tokenListItems = await findElements(driver, By.css('.token-list-item')) await tokenListItems[0].click() - await delay(regularDelayMs) + await delay(1000) - // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, - // or possibly until we use latest version of firefox in the tests - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance')) - await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000) - } + const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance')) + await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000) }) }) @@ -1141,9 +1127,6 @@ describe('MetaMask', function () { const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) await transferTokens.click() - if (process.env.SELENIUM_BROWSER !== 'firefox') { - await closeAllWindowHandlesExcept(driver, [extension, dapp]) - } await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1232,10 +1215,6 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - if (process.env.SELENIUM_BROWSER === 'firefox') { - this.skip() - } - await driver.wait(async () => { const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 3 @@ -1249,12 +1228,6 @@ describe('MetaMask', function () { }) describe('Tranfers a custom token from dapp when no gas value is specified', () => { - before(function () { - if (process.env.SELENIUM_BROWSER === 'firefox') { - this.skip() - } - }) - it('transfers an already created token, without specifying gas', async () => { const windowHandles = await driver.getAllWindowHandles() const extension = windowHandles[0] @@ -1267,7 +1240,6 @@ describe('MetaMask', function () { const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens Without Gas')]`)) await transferTokens.click() - await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1304,12 +1276,6 @@ describe('MetaMask', function () { }) describe('Approves a custom token from dapp when no gas value is specified', () => { - before(function () { - if (process.env.SELENIUM_BROWSER === 'firefox') { - this.skip() - } - }) - it('approves an already created token', async () => { const windowHandles = await driver.getAllWindowHandles() const extension = windowHandles[0] @@ -1323,7 +1289,6 @@ describe('MetaMask', function () { const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens Without Gas')]`)) await transferTokens.click() - await closeAllWindowHandlesExcept(driver, extension) await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1346,7 +1311,7 @@ describe('MetaMask', function () { }) it('submits the transaction', async function () { - await delay(regularDelayMs) + await delay(1000) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() await delay(regularDelayMs) diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh index 08c090da4..259926760 100755 --- a/test/e2e/run-all.sh +++ b/test/e2e/run-all.sh @@ -37,7 +37,6 @@ concurrently --kill-others \ 'yarn ganache:start' \ 'sleep 5 && mocha test/e2e/from-import-ui.spec' -export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" concurrently --kill-others \ --names 'ganache,e2e' \ --prefix '[{time}][{name}]' \ @@ -45,14 +44,13 @@ concurrently --kill-others \ 'npm run ganache:start' \ 'sleep 5 && mocha test/e2e/send-edit.spec' - - concurrently --kill-others \ - --names 'ganache,dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn ganache:start' \ - 'yarn dapp' \ - 'sleep 5 && mocha test/e2e/ethereum-on.spec' +concurrently --kill-others \ + --names 'ganache,dapp,e2e' \ + --prefix '[{time}][{name}]' \ + --success first \ + 'yarn ganache:start' \ + 'yarn dapp' \ + 'sleep 5 && mocha test/e2e/ethereum-on.spec' export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" concurrently --kill-others \ @@ -73,12 +71,11 @@ concurrently --kill-others \ 'sleep 5 && mocha test/e2e/address-book.spec' export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" - concurrently --kill-others \ - --names 'ganache,dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'node test/e2e/mock-3box/server.js' \ - 'yarn ganache:start' \ - 'yarn dapp' \ - 'sleep 5 && mocha test/e2e/threebox.spec' - \ No newline at end of file +concurrently --kill-others \ + --names 'ganache,dapp,e2e' \ + --prefix '[{time}][{name}]' \ + --success first \ + 'node test/e2e/mock-3box/server.js' \ + 'yarn ganache:start' \ + 'yarn dapp' \ + 'sleep 5 && mocha test/e2e/threebox.spec' diff --git a/test/e2e/send-edit.spec.js b/test/e2e/send-edit.spec.js index 5df8c86a2..d29245e3e 100644 --- a/test/e2e/send-edit.spec.js +++ b/test/e2e/send-edit.spec.js @@ -218,8 +218,10 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) assert.equal(txValues.length, 1) diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request.js index 16073d5d3..e7370c124 100644 --- a/ui/app/components/app/signature-request.js +++ b/ui/app/components/app/signature-request.js @@ -107,7 +107,7 @@ SignatureRequest.prototype.componentDidMount = function () { const { clearConfirmTransaction, cancel } = this.props const { metricsEvent } = this.context if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.onbeforeunload = event => { + this._onBeforeUnload = event => { metricsEvent({ eventOpts: { category: 'Transactions', @@ -118,6 +118,13 @@ SignatureRequest.prototype.componentDidMount = function () { clearConfirmTransaction() cancel(event) } + window.addEventListener('beforeunload', this._onBeforeUnload) + } +} + +SignatureRequest.prototype.componentWillUnmount = function () { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.removeEventListener('beforeunload', this._onBeforeUnload) } } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index ea8aa2dbd..e3d1ba44c 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -581,7 +581,7 @@ export default class ConfirmTransactionBase extends Component { }) if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.onbeforeunload = () => { + this._onBeforeUnload = () => { metricsEvent({ eventOpts: { category: 'Transactions', @@ -594,11 +594,18 @@ export default class ConfirmTransactionBase extends Component { }) cancelTransaction({ id }) } + window.addEventListener('beforeunload', this._onBeforeUnload) } getNextNonce() } + componentWillUnmount () { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.removeEventListener('beforeunload', this._onBeforeUnload) + } + } + render () { const { isTxReprice, diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js index 605b6ed92..e1c0b21ed 100644 --- a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js @@ -50,7 +50,7 @@ export default class ImportWithSeedPhrase extends PureComponent { } componentWillMount () { - window.onbeforeunload = () => this.context.metricsEvent({ + this._onBeforeUnload = () => this.context.metricsEvent({ eventOpts: { category: 'Onboarding', action: 'Import Seed Phrase', @@ -61,6 +61,11 @@ export default class ImportWithSeedPhrase extends PureComponent { errorMessage: this.state.seedPhraseError, }, }) + window.addEventListener('beforeunload', this._onBeforeUnload) + } + + componentWillUnmount () { + window.removeEventListener('beforeunload', this._onBeforeUnload) } handleSeedPhraseChange (seedPhrase) { diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 96962d685..59bad34bf 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -856,8 +856,6 @@ function signMsg (msgData) { log.debug('action - signMsg') return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null - return new Promise((resolve, reject) => { log.debug(`actions calling background.signMessage`) background.signMessage(msgData, (err, newState) => { @@ -884,7 +882,6 @@ function signPersonalMsg (msgData) { log.debug('action - signPersonalMsg') return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { log.debug(`actions calling background.signPersonalMessage`) background.signPersonalMessage(msgData, (err, newState) => { @@ -911,7 +908,6 @@ function signTypedMsg (msgData) { log.debug('action - signTypedMsg') return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { log.debug(`actions calling background.signTypedMessage`) background.signTypedMessage(msgData, (err, newState) => { @@ -1124,7 +1120,6 @@ function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) return (dispatch, getState) => { log.debug(`actions calling background.approveTransaction`) - window.onbeforeunload = null background.approveTransaction(txData.id, (err) => { if (err) { dispatch(actions.txError(err)) @@ -1201,7 +1196,6 @@ function updateAndApproveTx (txData) { return (dispatch) => { log.debug(`actions calling background.updateAndApproveTx`) dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { background.updateAndApproveTransaction(txData, err => { dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) @@ -1260,7 +1254,6 @@ function txError (err) { function cancelMsg (msgData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { log.debug(`background.cancelMessage`) background.cancelMessage(msgData.id, (err, newState) => { @@ -1283,7 +1276,6 @@ function cancelMsg (msgData) { function cancelPersonalMsg (msgData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { const id = msgData.id background.cancelPersonalMessage(id, (err, newState) => { @@ -1306,7 +1298,6 @@ function cancelPersonalMsg (msgData) { function cancelTypedMsg (msgData) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { const id = msgData.id background.cancelTypedMessage(id, (err, newState) => { @@ -1330,7 +1321,6 @@ function cancelTx (txData) { return (dispatch) => { log.debug(`background.cancelTransaction`) dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve, reject) => { background.cancelTransaction(txData.id, err => { if (err) { @@ -1360,7 +1350,6 @@ function cancelTx (txData) { */ function cancelTxs (txDataList) { return async (dispatch) => { - window.onbeforeunload = null dispatch(actions.showLoadingIndication()) const txIds = txDataList.map(({id}) => id) const cancellations = txIds.map((id) => new Promise((resolve, reject) => { @@ -1744,7 +1733,6 @@ function addTokens (tokens) { function removeSuggestedTokens () { return (dispatch) => { dispatch(actions.showLoadingIndication()) - window.onbeforeunload = null return new Promise((resolve) => { background.removeSuggestedTokens((err, suggestedTokens) => { dispatch(actions.hideLoadingIndication()) From 30606327f0b773600f6c226815a4392c6868dd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=A0?= Date: Thu, 31 Oct 2019 19:37:06 +0100 Subject: [PATCH 06/28] Add support for ZeroNet (#7038) --- app/scripts/lib/ens-ipfs/setup.js | 2 ++ package.json | 2 +- yarn.lock | 26 +++++++++++++------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 86f3e7d47..6b75adfa4 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -53,6 +53,8 @@ function setupEnsIpfsResolver ({ provider }) { url = `https://swarm-gateways.net/bzz:/${hash}${path}${search || ''}` } else if (type === 'onion' || type === 'onion3') { url = `http://${hash}.onion${path}${search || ''}` + } else if (type === 'zeronet') { + url = `http://127.0.0.1:43110/${hash}${path}${search || ''}` } } catch (err) { console.warn(err) diff --git a/package.json b/package.json index 84a1f0e24..3a8d5ea1a 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "c3": "^0.6.7", "classnames": "^2.2.5", "clone": "^2.1.2", - "content-hash": "^2.4.3", + "content-hash": "^2.4.4", "copy-to-clipboard": "^3.0.8", "currency-formatter": "^1.4.2", "d3": "^5.7.0", diff --git a/yarn.lock b/yarn.lock index b4753bcf4..d752122bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6924,13 +6924,13 @@ content-disposition@0.5.3, content-disposition@^0.5.2: dependencies: safe-buffer "5.1.2" -content-hash@^2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/content-hash/-/content-hash-2.4.3.tgz#89e6d295bbf2c53fb69b3d8fb43ae572ac7b6096" - integrity sha512-QBfQRlBBCJP94fV8zcUMChGMKzQMAZX6rn36yldc2A16C47tWdOTYjPosyZ7/AfdyW/xB5cP3RgZIAomnUDGIA== +content-hash@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/content-hash/-/content-hash-2.4.4.tgz#4bec87caecfcff8cf1a37645301cbef4728a083f" + integrity sha512-3FaUsqt7VR725pVxe0vIScGI5efmpryIXdVSeXafQ63fb5gRGparAQlAGxTSOiv0yRg7YeliseXuB20ByD1duQ== dependencies: cids "^0.6.0" - multicodec "^0.5.4" + multicodec "^0.5.5" multihashes "^0.4.15" content-type-parser@^1.0.1: @@ -18360,20 +18360,20 @@ multicast-dns@^7.2.0: dns-packet "^4.0.0" thunky "^1.0.2" -multicodec@^0.5.4, multicodec@~0.5.0: - version "0.5.4" - resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-0.5.4.tgz#ab2afb0cd00cd853c5a2eecb0dd1404dc7745388" - integrity sha512-0lPLiZ58b2jyXylx2qgda9/6N0YCNIpBxRsZ8sxYayVjEKh58XyNN74VTTQOR/ZCQFgbj0CsqfyRpEDPPlOMkw== - dependencies: - varint "^5.0.0" - -multicodec@~0.5.1, multicodec@~0.5.3: +multicodec@^0.5.5, multicodec@~0.5.1, multicodec@~0.5.3: version "0.5.5" resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-0.5.5.tgz#55c2535b44eca9ea40a13771420153fe075bb36d" integrity sha512-1kOifvwAqp9IdiiTKmpK2tS+LY6GHZdKpk3S2EvW4T32vlwDyA3hJoZtGauzqdedUPVNGChnTksEotVOCVlC+Q== dependencies: varint "^5.0.0" +multicodec@~0.5.0: + version "0.5.4" + resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-0.5.4.tgz#ab2afb0cd00cd853c5a2eecb0dd1404dc7745388" + integrity sha512-0lPLiZ58b2jyXylx2qgda9/6N0YCNIpBxRsZ8sxYayVjEKh58XyNN74VTTQOR/ZCQFgbj0CsqfyRpEDPPlOMkw== + dependencies: + varint "^5.0.0" + multihashes@^0.4.12: version "0.4.13" resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-0.4.13.tgz#d10bd71bd51d24aa894e2a6f1457146bb7bac125" From 6bd87e1f0937691e5eed6b92659c6b5e54164b9a Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Thu, 31 Oct 2019 21:56:02 -0230 Subject: [PATCH 07/28] Add web3 deprecation warning (#7334) * Add web3 deprecation warning * Update web3 deprecation article URL --- app/scripts/inpage.js | 21 --------------------- app/scripts/lib/auto-reload.js | 6 ++++++ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 31e6a1f49..ec88243a4 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -174,27 +174,6 @@ log.debug('MetaMask - injected web3') setupDappAutoReload(web3, inpageProvider.publicConfigStore) -// export global web3, with usage-detection and deprecation warning - -/* TODO: Uncomment this area once auto-reload.js has been deprecated: -let hasBeenWarned = false -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 - } - // return value normally - return _web3[key] - }, - set: (_web3, key, value) => { - // set value normally - _web3[key] = value - }, -}) -*/ - // set web3 defaultAccount inpageProvider.publicConfigStore.subscribe(function (state) { web3.eth.defaultAccount = state.selectedAddress diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 44fbe847c..fd209c230 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -5,11 +5,17 @@ function setupDappAutoReload (web3, observable) { let reloadInProgress = false let lastTimeUsed let lastSeenNetwork + let hasBeenWarned = false global.web3 = new Proxy(web3, { get: (_web3, key) => { // get the time of use lastTimeUsed = Date.now() + // 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://medium.com/metamask/4a899ad6e59e') + hasBeenWarned = true + } // return value normally return _web3[key] }, From f9cd775eae5195f1d7ca4ba7c81f77cdda402ac5 Mon Sep 17 00:00:00 2001 From: Kristian Tapia Date: Thu, 31 Oct 2019 18:51:28 -0700 Subject: [PATCH 08/28] Add Estimated time to pending tx (#6924) * Add estimated time to pending transactions * add sytles for pending transactions component * add media queries styling for pending transactions component * fix lint errors, remove extra spaces * refactor code to call `fetchBasicGasAndTimeEstimates` method once * refactor code to call `getgetRenderableTimeEstimate` method once * fix, correct export to use `transaction-time-remaining-component` * fix indentation issues after running `yarn lint` * newBigSigDig in gas-price-chart.utils supports strings * Code cleanup * Ensure fetchBasicGasAndTimeEstimates is only called from tx-list if there are pending-txs * Move gas time estimate utilities into utility file * Move getTxParams to transaction selector file * Add feature flag for display of remaining transaction time in tx history list * Fix circular dependency by removing unused import of transactionSelector in selectors.js * Use correct feature flag property name transactionTime * Ensure that tx list component correctly responds to turning tx time feature on * Prevent precision errors in newBigSigDig * Code clean up for pending transaction times * Update transaction-time-remaining feature to count down seconds, countdown seconds and show '< 30' * Code clean up for transaction-time-remaining feature --- app/scripts/controllers/preferences.js | 1 + test/unit/ui/app/selectors.spec.js | 6 -- .../gas-modal-page-container.container.js | 47 +-------- .../gas-price-chart/gas-price-chart.utils.js | 32 ++---- .../app/transaction-list-item/index.scss | 22 ++++- .../transaction-list-item.component.js | 25 +++-- .../transaction-list-item.container.js | 14 ++- .../transaction-list.component.js | 38 ++++++- .../transaction-list.container.js | 13 ++- .../app/transaction-time-remaining/index.js | 1 + .../transaction-time-remaining.component.js | 52 ++++++++++ .../transaction-time-remaining.container.js | 41 ++++++++ .../transaction-time-remaining.util.js | 13 +++ .../helpers/utils/gas-time-estimates.util.js | 99 +++++++++++++++++++ ui/app/selectors/selectors.js | 5 - ui/app/selectors/transactions.js | 14 +++ 16 files changed, 325 insertions(+), 98 deletions(-) create mode 100644 ui/app/components/app/transaction-time-remaining/index.js create mode 100644 ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js create mode 100644 ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js create mode 100644 ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js create mode 100644 ui/app/helpers/utils/gas-time-estimates.util.js diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index dfeeaeb82..1cfbb4d4c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -44,6 +44,7 @@ class PreferencesController { // perform sensitive operations. featureFlags: { showIncomingTransactions: true, + transactionTime: false, }, knownMethodData: {}, participateInMetaMetrics: null, diff --git a/test/unit/ui/app/selectors.spec.js b/test/unit/ui/app/selectors.spec.js index a190462b0..f8d58f61c 100644 --- a/test/unit/ui/app/selectors.spec.js +++ b/test/unit/ui/app/selectors.spec.js @@ -109,12 +109,6 @@ describe('Selectors', function () { assert.equal(currentAccountwithSendEther.name, 'Test Account') }) - describe('#transactionSelector', function () { - it('returns transactions from state', function () { - selectors.transactionsSelector(mockState) - }) - }) - it('#getGasIsLoading', () => { const gasIsLoading = selectors.getGasIsLoading(mockState) assert.equal(gasIsLoading, false) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index bf17a049a..520946a95 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -34,8 +34,6 @@ import { preferencesSelector, } from '../../../../selectors/selectors.js' import { - formatTimeEstimate, - getFastPriceEstimateInHexWEI, getBasicGasEstimateLoadingStatus, getGasEstimatesLoadingStatus, getCustomGasLimit, @@ -47,6 +45,9 @@ import { getBasicGasEstimateBlockTime, isCustomPriceSafe, } from '../../../../selectors/custom-gas' +import { + getTxParams, +} from '../../../../selectors/transactions' import { getTokenBalance, } from '../../../../pages/send/send.selectors' @@ -59,6 +60,7 @@ import { decEthToConvertedCurrency as ethTotalToConvertedCurrency, hexWEIToDecGWEI, } from '../../../../helpers/utils/conversions.util' +import { getRenderableTimeEstimate } from '../../../../helpers/utils/gas-time-estimates.util' import { formatETHFee, } from '../../../../helpers/utils/formatters' @@ -67,7 +69,6 @@ import { isBalanceSufficient, } from '../../../../pages/send/send.utils' import { addHexPrefix } from 'ethereumjs-util' -import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils' import { getMaxModeOn } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors' import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils' @@ -301,18 +302,6 @@ function calcCustomGasLimit (customGasLimitInHex) { return parseInt(customGasLimitInHex, 16) } -function getTxParams (state, selectedTransaction = {}) { - const { metamask: { send } } = state - const { txParams } = selectedTransaction - return txParams || { - from: send.from, - gas: send.gasLimit || '0x5208', - gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), - to: send.to, - value: getSelectedToken(state) ? '0x0' : send.amount, - } -} - function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) { return pipe( addHexWEIsToDec, @@ -334,31 +323,3 @@ function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conver partialRight(formatCurrency, [convertedCurrency]), )(aHexWEI, bHexWEI) } - -function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { - const minGasPrice = gasPrices[0] - const maxGasPrice = gasPrices[gasPrices.length - 1] - let priceForEstimation = currentGasPrice - if (currentGasPrice < minGasPrice) { - priceForEstimation = minGasPrice - } else if (currentGasPrice > maxGasPrice) { - priceForEstimation = maxGasPrice - } - - const { - closestLowerValueIndex, - closestHigherValueIndex, - closestHigherValue, - closestLowerValue, - } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) - - const newTimeEstimate = extrapolateY({ - higherY: estimatedTimes[closestHigherValueIndex], - lowerY: estimatedTimes[closestLowerValueIndex], - higherX: closestHigherValue, - lowerX: closestLowerValue, - xForExtrapolation: priceForEstimation, - }) - - return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) -} diff --git a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js index b941f1cf9..419cae0cd 100644 --- a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js +++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -1,11 +1,12 @@ import * as d3 from 'd3' import c3 from 'c3' -import BigNumber from 'bignumber.js' - -const newBigSigDig = n => (new BigNumber(n.toPrecision(15))) -const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b)) -const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus') -const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div') +import { + extrapolateY, + getAdjacentGasPrices, + newBigSigDig, + bigNumMinus, + bigNumDiv, +} from '../../../../helpers/utils/gas-time-estimates.util' export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) { const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({ @@ -66,25 +67,6 @@ export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) { } } -export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { - const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) - const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition) - return { - closestLowerValueIndex, - closestHigherValueIndex, - closestHigherValue: gasPrices[closestHigherValueIndex], - closestLowerValue: gasPrices[closestLowerValueIndex], - } -} - -export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) { - const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX)) - const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated() - - return newTimeEstimate.toNumber() -} - - export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) { const chartMouseXPos = bigNumMinus(xMousePos, chartXStart) const posPercentile = bigNumDiv(chartMouseXPos, chartWidth) diff --git a/ui/app/components/app/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss index 02732768e..54a2e9db3 100644 --- a/ui/app/components/app/transaction-list-item/index.scss +++ b/ui/app/components/app/transaction-list-item/index.scss @@ -15,17 +15,17 @@ display: grid; grid-template-columns: 45px 1fr 1fr 1fr; grid-template-areas: - "identicon action status primary-amount" - "identicon nonce status secondary-amount"; + "identicon action status estimated-time primary-amount" + "identicon nonce status estimated-time secondary-amount"; grid-template-rows: 24px; @media screen and (max-width: $break-small) { padding: .5rem 1rem; grid-template-columns: 45px 5fr 3fr; grid-template-areas: - "nonce nonce nonce" - "identicon action primary-amount" - "identicon status secondary-amount"; + "nonce nonce nonce nonce" + "identicon action estimated-time primary-amount" + "identicon status estimated-time secondary-amount"; grid-template-rows: auto 24px; } @@ -65,6 +65,18 @@ } } + &__estimated-time { + grid-area: estimated-time; + grid-row: 1 / span 2; + align-self: center; + + @media screen and (max-width: $break-small) { + grid-row: 3; + grid-column: 4; + font-size: small; + } + } + &__nonce { font-size: .75rem; color: #5e6064; diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index bb6acae68..d1d6f061d 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -7,6 +7,7 @@ import TransactionAction from '../transaction-action' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import TokenCurrencyDisplay from '../../ui/token-currency-display' import TransactionListItemDetails from '../transaction-list-item-details' +import TransactionTimeRemaining from '../transaction-time-remaining' import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions' import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' @@ -38,6 +39,8 @@ export default class TransactionListItem extends PureComponent { data: PropTypes.string, getContractMethodData: PropTypes.func, isDeposit: PropTypes.bool, + transactionTimeFeatureActive: PropTypes.bool, + firstPendingTransactionId: PropTypes.number, } static defaultProps = { @@ -52,6 +55,13 @@ export default class TransactionListItem extends PureComponent { showTransactionDetails: false, } + componentDidMount () { + if (this.props.data) { + this.props.getContractMethodData(this.props.data) + } + + } + handleClick = () => { const { transaction, @@ -162,12 +172,6 @@ export default class TransactionListItem extends PureComponent { ) } - componentDidMount () { - if (this.props.data) { - this.props.getContractMethodData(this.props.data) - } - } - render () { const { assetImages, @@ -182,6 +186,8 @@ export default class TransactionListItem extends PureComponent { transactionGroup, rpcPrefs, isEarliestNonce, + firstPendingTransactionId, + transactionTimeFeatureActive, } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state @@ -221,6 +227,13 @@ export default class TransactionListItem extends PureComponent { : primaryTransaction.err && primaryTransaction.err.message )} /> + { transactionTimeFeatureActive && (transaction.id === firstPendingTransactionId) + ? + : null + } { this.renderPrimaryCurrency() } { this.renderSecondaryCurrency() } diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js index c3cf0295b..26ccec1f7 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js @@ -8,12 +8,19 @@ import { getTokenData } from '../../../helpers/utils/transactions.util' import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util' import { formatDate } from '../../../helpers/utils/util' import { - fetchBasicGasAndTimeEstimates, fetchGasEstimates, + fetchBasicGasAndTimeEstimates, setCustomGasPriceForRetry, setCustomGasLimit, } from '../../../ducks/gas/gas.duck' -import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSelector, getKnownMethodData } from '../../../selectors/selectors' +import { + getIsMainnet, + preferencesSelector, + getSelectedAddress, + conversionRateSelector, + getKnownMethodData, + getFeatureFlags, +} from '../../../selectors/selectors' import { isBalanceSufficient } from '../../../pages/send/send.utils' const mapStateToProps = (state, ownProps) => { @@ -38,6 +45,8 @@ const mapStateToProps = (state, ownProps) => { conversionRate: conversionRateSelector(state), }) + const transactionTimeFeatureActive = getFeatureFlags(state).transactionTime + return { methodData: getKnownMethodData(state, data) || {}, showFiat: (isMainnet || !!showFiatInTestnets), @@ -45,6 +54,7 @@ const mapStateToProps = (state, ownProps) => { hasEnoughCancelGas, rpcPrefs, isDeposit, + transactionTimeFeatureActive, } } diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index 553a0ffc4..0e0540257 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -22,19 +22,50 @@ export default class TransactionList extends PureComponent { selectedToken: PropTypes.object, updateNetworkNonce: PropTypes.func, assetImages: PropTypes.object, + fetchBasicGasAndTimeEstimates: PropTypes.func, + fetchGasEstimates: PropTypes.func, + transactionTimeFeatureActive: PropTypes.bool, + firstPendingTransactionId: PropTypes.number, } componentDidMount () { - this.props.updateNetworkNonce() + const { + pendingTransactions, + updateNetworkNonce, + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, + transactionTimeFeatureActive, + } = this.props + + updateNetworkNonce() + + if (transactionTimeFeatureActive && pendingTransactions.length) { + fetchBasicGasAndTimeEstimates() + .then(({ blockTime }) => fetchGasEstimates(blockTime)) + } } componentDidUpdate (prevProps) { const { pendingTransactions: prevPendingTransactions = [] } = prevProps - const { pendingTransactions = [], updateNetworkNonce } = this.props + const { + pendingTransactions = [], + updateNetworkNonce, + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, + transactionTimeFeatureActive, + } = this.props if (pendingTransactions.length > prevPendingTransactions.length) { updateNetworkNonce() } + + const transactionTimeFeatureWasActivated = !prevProps.transactionTimeFeatureActive && transactionTimeFeatureActive + const pendingTransactionAdded = pendingTransactions.length > 0 && prevPendingTransactions.length === 0 + + if (transactionTimeFeatureActive && pendingTransactions.length > 0 && (transactionTimeFeatureWasActivated || pendingTransactionAdded)) { + fetchBasicGasAndTimeEstimates() + .then(({ blockTime }) => fetchGasEstimates(blockTime)) + } } shouldShowSpeedUp = (transactionGroup, isEarliestNonce) => { @@ -87,7 +118,7 @@ export default class TransactionList extends PureComponent { } renderTransaction (transactionGroup, index, isPendingTx = false) { - const { selectedToken, assetImages } = this.props + const { selectedToken, assetImages, firstPendingTransactionId } = this.props const { transactions = [] } = transactionGroup return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT @@ -105,6 +136,7 @@ export default class TransactionList extends PureComponent { isEarliestNonce={isPendingTx && index === 0} token={selectedToken} assetImages={assetImages} + firstPendingTransactionId={firstPendingTransactionId} /> ) } diff --git a/ui/app/components/app/transaction-list/transaction-list.container.js b/ui/app/components/app/transaction-list/transaction-list.container.js index 67a24588b..4da044b2a 100644 --- a/ui/app/components/app/transaction-list/transaction-list.container.js +++ b/ui/app/components/app/transaction-list/transaction-list.container.js @@ -6,23 +6,30 @@ import { nonceSortedCompletedTransactionsSelector, nonceSortedPendingTransactionsSelector, } from '../../../selectors/transactions' -import { getSelectedAddress, getAssetImages } from '../../../selectors/selectors' +import { getSelectedAddress, getAssetImages, getFeatureFlags } from '../../../selectors/selectors' import { selectedTokenSelector } from '../../../selectors/tokens' import { updateNetworkNonce } from '../../../store/actions' +import { fetchBasicGasAndTimeEstimates, fetchGasEstimates } from '../../../ducks/gas/gas.duck' -const mapStateToProps = state => { +const mapStateToProps = (state) => { + const pendingTransactions = nonceSortedPendingTransactionsSelector(state) + const firstPendingTransactionId = pendingTransactions[0] && pendingTransactions[0].primaryTransaction.id return { completedTransactions: nonceSortedCompletedTransactionsSelector(state), - pendingTransactions: nonceSortedPendingTransactionsSelector(state), + pendingTransactions, + firstPendingTransactionId, selectedToken: selectedTokenSelector(state), selectedAddress: getSelectedAddress(state), assetImages: getAssetImages(state), + transactionTimeFeatureActive: getFeatureFlags(state).transactionTime, } } const mapDispatchToProps = dispatch => { return { updateNetworkNonce: address => dispatch(updateNetworkNonce(address)), + fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), } } diff --git a/ui/app/components/app/transaction-time-remaining/index.js b/ui/app/components/app/transaction-time-remaining/index.js new file mode 100644 index 000000000..87c6821d8 --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/index.js @@ -0,0 +1 @@ +export { default } from './transaction-time-remaining.container' diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js new file mode 100644 index 000000000..c9598d69b --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js @@ -0,0 +1,52 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { calcTransactionTimeRemaining } from './transaction-time-remaining.util' + +export default class TransactionTimeRemaining extends PureComponent { + static propTypes = { + className: PropTypes.string, + initialTimeEstimate: PropTypes.number, + submittedTime: PropTypes.number, + } + + constructor (props) { + super(props) + const { initialTimeEstimate, submittedTime } = props + this.state = { + timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime), + } + this.interval = setInterval( + () => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }), + 1000 + ) + } + + componentDidUpdate (prevProps) { + const { initialTimeEstimate, submittedTime } = this.props + if (initialTimeEstimate !== prevProps.initialTimeEstimate) { + clearInterval(this.interval) + const calcedTimeRemaining = calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) + this.setState({ timeRemaining: calcedTimeRemaining }) + this.interval = setInterval( + () => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }), + 1000 + ) + } + } + + componentWillUnmount () { + clearInterval(this.interval) + } + + render () { + const { className } = this.props + const { timeRemaining } = this.state + + return ( +
+ { timeRemaining } +
+ + ) + } +} diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js new file mode 100644 index 000000000..65eeaa0c3 --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js @@ -0,0 +1,41 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import TransactionTimeRemaining from './transaction-time-remaining.component' +import { + getTxParams, +} from '../../../selectors/transactions' +import { + getEstimatedGasPrices, + getEstimatedGasTimes, +} from '../../../selectors/custom-gas' +import { getRawTimeEstimateData } from '../../../helpers/utils/gas-time-estimates.util' +import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util' + +const mapStateToProps = (state, ownProps) => { + const { transaction } = ownProps + const { gasPrice: currentGasPrice } = getTxParams(state, transaction) + const customGasPrice = calcCustomGasPrice(currentGasPrice) + const gasPrices = getEstimatedGasPrices(state) + const estimatedTimes = getEstimatedGasTimes(state) + + const { + newTimeEstimate: initialTimeEstimate, + } = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes) + + const submittedTime = transaction.submittedTime + + return { + initialTimeEstimate, + submittedTime, + } +} + +export default compose( + withRouter, + connect(mapStateToProps) +)(TransactionTimeRemaining) + +function calcCustomGasPrice (customGasPriceInHex) { + return Number(hexWEIToDecGWEI(customGasPriceInHex)) +} diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js new file mode 100644 index 000000000..0ba81edfc --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js @@ -0,0 +1,13 @@ +import { formatTimeEstimate } from '../../../helpers/utils/gas-time-estimates.util' + +export function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) { + const currentTime = (new Date()).getTime() + const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000 + const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission + + const renderingTimeRemainingEstimate = timeRemainingOnEstimate < 30 + ? '< 30 s' + : formatTimeEstimate(timeRemainingOnEstimate) + + return renderingTimeRemainingEstimate +} diff --git a/ui/app/helpers/utils/gas-time-estimates.util.js b/ui/app/helpers/utils/gas-time-estimates.util.js new file mode 100644 index 000000000..7e143a028 --- /dev/null +++ b/ui/app/helpers/utils/gas-time-estimates.util.js @@ -0,0 +1,99 @@ +import BigNumber from 'bignumber.js' + +export function newBigSigDig (n) { + return new BigNumber((new BigNumber(String(n))).toPrecision(15)) +} + +const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b)) + +export function bigNumMinus (a = 0, b = 0) { + return createOp(a, b, 'minus') +} + +export function bigNumDiv (a = 0, b = 1) { + return createOp(a, b, 'div') +} + +export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) { + const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX)) + const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated() + + return newTimeEstimate.toNumber() +} + +export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { + const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) + const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition) + return { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue: gasPrices[closestHigherValueIndex], + closestLowerValue: gasPrices[closestLowerValueIndex], + } +} + +export function formatTimeEstimate (totalSeconds, greaterThanMax, lessThanMin) { + const minutes = Math.floor(totalSeconds / 60) + const seconds = Math.floor(totalSeconds % 60) + + if (!minutes && !seconds) { + return '...' + } + + let symbol = '~' + if (greaterThanMax) { + symbol = '< ' + } else if (lessThanMin) { + symbol = '> ' + } + + const formattedMin = `${minutes ? minutes + ' min' : ''}` + const formattedSec = `${seconds ? seconds + ' sec' : ''}` + const formattedCombined = formattedMin && formattedSec + ? `${symbol}${formattedMin} ${formattedSec}` + : symbol + (formattedMin || formattedSec) + + return formattedCombined +} + +export function getRawTimeEstimateData (currentGasPrice, gasPrices, estimatedTimes) { + const minGasPrice = gasPrices[0] + const maxGasPrice = gasPrices[gasPrices.length - 1] + let priceForEstimation = currentGasPrice + if (currentGasPrice < minGasPrice) { + priceForEstimation = minGasPrice + } else if (currentGasPrice > maxGasPrice) { + priceForEstimation = maxGasPrice + } + + const { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue, + closestLowerValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) + + const newTimeEstimate = extrapolateY({ + higherY: estimatedTimes[closestHigherValueIndex], + lowerY: estimatedTimes[closestLowerValueIndex], + higherX: closestHigherValue, + lowerX: closestLowerValue, + xForExtrapolation: priceForEstimation, + }) + + return { + newTimeEstimate, + minGasPrice, + maxGasPrice, + } +} + +export function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { + const { + newTimeEstimate, + minGasPrice, + maxGasPrice, + } = getRawTimeEstimateData(currentGasPrice, gasPrices, estimatedTimes) + + return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) +} diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 943ceba0c..fab5f1dae 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -1,11 +1,7 @@ import { NETWORK_TYPES } from '../helpers/constants/common' import { stripHexPrefix, addHexPrefix } from 'ethereumjs-util' - const abi = require('human-standard-token-abi') -import { - transactionsSelector, -} from './transactions' const { multiplyCurrencies, } = require('../helpers/utils/conversion-util') @@ -24,7 +20,6 @@ const selectors = { getAssetImages, getTokenExchangeRate, conversionRateSelector, - transactionsSelector, accountsWithSendEtherInfoSelector, getCurrentAccountWithSendEtherInfo, getGasIsLoading, diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js index 2a6a92ddf..e25bb5be0 100644 --- a/ui/app/selectors/transactions.js +++ b/ui/app/selectors/transactions.js @@ -11,6 +11,8 @@ import { } from '../../../app/scripts/controllers/transactions/enums' import { hexToDecimal } from '../helpers/utils/conversions.util' import { selectedTokenAddressSelector } from './tokens' +import { getFastPriceEstimateInHexWEI } from './custom-gas' +import { getSelectedToken } from './selectors' import txHelper from '../../lib/tx-helper' export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList @@ -303,3 +305,15 @@ export const submittedPendingTransactionsSelector = createSelector( transactions.filter(transaction => transaction.status === SUBMITTED_STATUS) ) ) + +export const getTxParams = (state, selectedTransaction = {}) => { + const { metamask: { send } } = state + const { txParams } = selectedTransaction + return txParams || { + from: send.from, + gas: send.gasLimit || '0x5208', + gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), + to: send.to, + value: getSelectedToken(state) ? '0x0' : send.amount, + } +} From eed4a9ed6547c76299da086916168c48a4e8fef4 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Fri, 1 Nov 2019 15:24:00 -0230 Subject: [PATCH 09/28] ENS Reverse Resolution support (#7177) * ENS Reverse Resolution support * Save punycode for ENS domains with Unicode characters * Update SenderToRecipient recipientEns tooltip * Use cached results when reverse-resolving ENS names * Display ENS names in tx activity log --- app/scripts/controllers/ens/ens.js | 25 ++++ app/scripts/controllers/ens/index.js | 75 ++++++++++ app/scripts/metamask-controller.js | 11 ++ package.json | 1 + .../app/controllers/ens-controller-test.js | 135 ++++++++++++++++++ .../confirm-page-container.component.js | 3 + .../transaction-list-item-details/index.js | 2 +- ...transaction-list-item-details.component.js | 24 +++- ...transaction-list-item-details.container.js | 28 ++++ .../transaction-list-item.component.js | 3 + .../sender-to-recipient.component.js | 42 ++++-- ui/app/components/ui/tooltip-v2.js | 7 +- .../confirm-transaction-base.component.js | 7 +- .../confirm-transaction-base.container.js | 10 +- ui/app/store/actions.js | 15 ++ yarn.lock | 2 +- 16 files changed, 373 insertions(+), 17 deletions(-) create mode 100644 app/scripts/controllers/ens/ens.js create mode 100644 app/scripts/controllers/ens/index.js create mode 100644 test/unit/app/controllers/ens-controller-test.js create mode 100644 ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js diff --git a/app/scripts/controllers/ens/ens.js b/app/scripts/controllers/ens/ens.js new file mode 100644 index 000000000..eb2586a7d --- /dev/null +++ b/app/scripts/controllers/ens/ens.js @@ -0,0 +1,25 @@ +const EthJsEns = require('ethjs-ens') +const ensNetworkMap = require('ethjs-ens/lib/network-map.json') + +class Ens { + static getNetworkEnsSupport (network) { + return Boolean(ensNetworkMap[network]) + } + + constructor ({ network, provider } = {}) { + this._ethJsEns = new EthJsEns({ + network, + provider, + }) + } + + lookup (ensName) { + return this._ethJsEns.lookup(ensName) + } + + reverse (address) { + return this._ethJsEns.reverse(address) + } +} + +module.exports = Ens diff --git a/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js new file mode 100644 index 000000000..6456f8b53 --- /dev/null +++ b/app/scripts/controllers/ens/index.js @@ -0,0 +1,75 @@ +const ethUtil = require('ethereumjs-util') +const ObservableStore = require('obs-store') +const punycode = require('punycode') +const Ens = require('./ens') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +class EnsController { + constructor ({ ens, provider, networkStore } = {}) { + const initState = { + ensResolutionsByAddress: {}, + } + + this._ens = ens + if (!this._ens) { + const network = networkStore.getState() + if (Ens.getNetworkEnsSupport(network)) { + this._ens = new Ens({ + network, + provider, + }) + } + } + + this.store = new ObservableStore(initState) + networkStore.subscribe((network) => { + this.store.putState(initState) + this._ens = new Ens({ + network, + provider, + }) + }) + } + + reverseResolveAddress (address) { + return this._reverseResolveAddress(ethUtil.toChecksumAddress(address)) + } + + async _reverseResolveAddress (address) { + if (!this._ens) { + return undefined + } + + const state = this.store.getState() + if (state.ensResolutionsByAddress[address]) { + return state.ensResolutionsByAddress[address] + } + + const domain = await this._ens.reverse(address) + const registeredAddress = await this._ens.lookup(domain) + if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) { + return undefined + } + + if (ethUtil.toChecksumAddress(registeredAddress) !== address) { + return undefined + } + + this._updateResolutionsByAddress(address, punycode.toASCII(domain)) + return domain + } + + _updateResolutionsByAddress (address, domain) { + const oldState = this.store.getState() + this.store.putState({ + ensResolutionsByAddress: { + ...oldState.ensResolutionsByAddress, + [address]: domain, + }, + }) + } +} + +module.exports = EnsController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index eac6d1e81..1c607a4c6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -23,6 +23,7 @@ const createLoggerMiddleware = require('./lib/createLoggerMiddleware') const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware') const {setupMultiplex} = require('./lib/stream-utils.js') const KeyringController = require('eth-keyring-controller') +const EnsController = require('./controllers/ens') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const AppStateController = require('./controllers/app-state') @@ -138,6 +139,11 @@ module.exports = class MetamaskController extends EventEmitter { networkController: this.networkController, }) + this.ensController = new EnsController({ + provider: this.provider, + networkStore: this.networkController.networkStore, + }) + this.incomingTransactionsController = new IncomingTransactionsController({ blockTracker: this.blockTracker, networkController: this.networkController, @@ -315,6 +321,8 @@ module.exports = class MetamaskController extends EventEmitter { // ThreeBoxController ThreeBoxController: this.threeBoxController.store, ABTestController: this.abTestController.store, + // ENS Controller + EnsController: this.ensController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -501,6 +509,9 @@ module.exports = class MetamaskController extends EventEmitter { // AppStateController setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), + // EnsController + tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController), + // KeyringController setLocked: nodeify(this.setLocked, this), createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), diff --git a/package.json b/package.json index 3a8d5ea1a..5c155f5ea 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "prop-types": "^15.6.1", "pubnub": "4.24.4", "pump": "^3.0.0", + "punycode": "^2.1.1", "qrcode-generator": "1.4.1", "ramda": "^0.24.1", "react": "^15.6.2", diff --git a/test/unit/app/controllers/ens-controller-test.js b/test/unit/app/controllers/ens-controller-test.js new file mode 100644 index 000000000..1eb52a17c --- /dev/null +++ b/test/unit/app/controllers/ens-controller-test.js @@ -0,0 +1,135 @@ +const assert = require('assert') +const sinon = require('sinon') +const ObservableStore = require('obs-store') +const HttpProvider = require('ethjs-provider-http') +const EnsController = require('../../../../app/scripts/controllers/ens') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +describe('EnsController', function () { + describe('#constructor', function () { + it('should construct the controller given a provider and a network', async () => { + const provider = new HttpProvider('https://ropsten.infura.io') + const currentNetworkId = '3' + const networkStore = new ObservableStore(currentNetworkId) + const ens = new EnsController({ + provider, + networkStore, + }) + + assert.ok(ens._ens) + }) + + it('should construct the controller given an existing ENS instance', async () => { + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: {}, + networkStore, + }) + + assert.ok(ens._ens) + }) + }) + + describe('#reverseResolveName', function () { + it('should resolve to an ENS name', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(address), + }, + networkStore, + }) + + const name = await ens.reverseResolveAddress(address) + assert.equal(name, 'peaksignal.eth') + }) + + it('should only resolve an ENS name once', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const reverse = sinon.stub().withArgs(address).returns('peaksignal.eth') + const lookup = sinon.stub().withArgs('peaksignal.eth').returns(address) + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse, + lookup, + }, + networkStore, + }) + + assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') + assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') + assert.ok(lookup.calledOnce) + assert.ok(reverse.calledOnce) + }) + + it('should fail if the name is registered to a different address than the reverse-resolved', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns('0xfoo'), + }, + networkStore, + }) + + const name = await ens.reverseResolveAddress(address) + assert.strictEqual(name, undefined) + }) + + it('should throw an error when the lookup resolves to the zero address', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_ADDRESS), + }, + networkStore, + }) + + try { + await ens.reverseResolveAddress(address) + assert.fail('#reverseResolveAddress did not throw') + } catch (e) { + assert.ok(e) + } + }) + + it('should throw an error the lookup resolves to the zero x address', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_X_ERROR_ADDRESS), + }, + networkStore, + }) + + try { + await ens.reverseResolveAddress(address) + assert.fail('#reverseResolveAddress did not throw') + } catch (e) { + assert.ok(e) + } + }) + }) +}) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index d26daf786..41d9d5952 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -24,6 +24,7 @@ export default class ConfirmPageContainer extends Component { fromName: PropTypes.string, toAddress: PropTypes.string, toName: PropTypes.string, + toEns: PropTypes.string, toNickname: PropTypes.string, // Content contentComponent: PropTypes.node, @@ -69,6 +70,7 @@ export default class ConfirmPageContainer extends Component { fromName, fromAddress, toName, + toEns, toNickname, toAddress, disabled, @@ -128,6 +130,7 @@ export default class ConfirmPageContainer extends Component { senderAddress={fromAddress} recipientName={toName} recipientAddress={toAddress} + recipientEns={toEns} recipientNickname={toNickname} assetImage={renderAssetImage ? assetImage : undefined} /> diff --git a/ui/app/components/app/transaction-list-item-details/index.js b/ui/app/components/app/transaction-list-item-details/index.js index 0e878d032..83bd53e7d 100644 --- a/ui/app/components/app/transaction-list-item-details/index.js +++ b/ui/app/components/app/transaction-list-item-details/index.js @@ -1 +1 @@ -export { default } from './transaction-list-item-details.component' +export { default } from './transaction-list-item-details.container' diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 983bbf6e5..f27c74970 100644 --- a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -17,6 +17,10 @@ export default class TransactionListItemDetails extends PureComponent { metricsEvent: PropTypes.func, } + static defaultProps = { + recipientEns: null, + } + static propTypes = { onCancel: PropTypes.func, onRetry: PropTypes.func, @@ -26,7 +30,11 @@ export default class TransactionListItemDetails extends PureComponent { isEarliestNonce: PropTypes.bool, cancelDisabled: PropTypes.bool, transactionGroup: PropTypes.object, + recipientEns: PropTypes.string, + recipientAddress: PropTypes.string.isRequired, rpcPrefs: PropTypes.object, + senderAddress: PropTypes.string.isRequired, + tryReverseResolveAddress: PropTypes.func.isRequired, } state = { @@ -82,6 +90,12 @@ export default class TransactionListItemDetails extends PureComponent { }) } + async componentDidMount () { + const { recipientAddress, tryReverseResolveAddress } = this.props + + tryReverseResolveAddress(recipientAddress) + } + renderCancel () { const { t } = this.context const { @@ -128,11 +142,14 @@ export default class TransactionListItemDetails extends PureComponent { showRetry, onCancel, onRetry, + recipientEns, + recipientAddress, rpcPrefs: { blockExplorerUrl } = {}, + senderAddress, isEarliestNonce, } = this.props const { primaryTransaction: transaction } = transactionGroup - const { hash, txParams: { to, from } = {} } = transaction + const { hash } = transaction return (
@@ -192,8 +209,9 @@ export default class TransactionListItemDetails extends PureComponent { { this.context.metricsEvent({ eventOpts: { diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js new file mode 100644 index 000000000..50f93f497 --- /dev/null +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux' +import TransactionListItemDetails from './transaction-list-item-details.component' +import { checksumAddress } from '../../../helpers/utils/util' +import { tryReverseResolveAddress } from '../../../store/actions' + +const mapStateToProps = (state, ownProps) => { + const { metamask } = state + const { + ensResolutionsByAddress, + } = metamask + const { recipientAddress } = ownProps + const address = checksumAddress(recipientAddress) + const recipientEns = ensResolutionsByAddress[address] || '' + + return { + recipientEns, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + tryReverseResolveAddress: (address) => { + return dispatch(tryReverseResolveAddress(address)) + }, + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(TransactionListItemDetails) diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index d1d6f061d..12350ada6 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -191,6 +191,7 @@ export default class TransactionListItem extends PureComponent { } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state + const fromAddress = txParams.from const toAddress = tokenData ? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to : txParams.to @@ -253,6 +254,8 @@ export default class TransactionListItem extends PureComponent { showCancel={showCancel} cancelDisabled={!hasEnoughCancelGas} rpcPrefs={rpcPrefs} + senderAddress={fromAddress} + recipientAddress={toAddress} />
) diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index c8e7a1870..3102f17e3 100644 --- a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -5,7 +5,7 @@ import Identicon from '../identicon' import Tooltip from '../tooltip-v2' import copyToClipboard from 'copy-to-clipboard' import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants' -import { checksumAddress } from '../../../helpers/utils/util' +import { checksumAddress, addressSlicer } from '../../../helpers/utils/util' const variantHash = { [DEFAULT_VARIANT]: 'sender-to-recipient--default', @@ -18,6 +18,7 @@ export default class SenderToRecipient extends PureComponent { senderName: PropTypes.string, senderAddress: PropTypes.string, recipientName: PropTypes.string, + recipientEns: PropTypes.string, recipientAddress: PropTypes.string, recipientNickname: PropTypes.string, t: PropTypes.func, @@ -60,14 +61,28 @@ export default class SenderToRecipient extends PureComponent { return ( {t('copiedExclamation')}

+ : addressOnly + ?

{t('copyAddress')}

+ : ( +

+ {addressSlicer(checksummedSenderAddress)}
+ {t('copyAddress')} +

+ ) + } wrapperClassName="sender-to-recipient__tooltip-wrapper" containerClassName="sender-to-recipient__tooltip-container" onHidden={() => this.setState({ senderAddressCopied: false })} >
- { addressOnly ? `${t('from')}: ` : '' } - { addressOnly ? checksummedSenderAddress : senderName } + { + addressOnly + ? {`${t('from')}: ${checksummedSenderAddress}`} + : senderName + }
) @@ -90,7 +105,7 @@ export default class SenderToRecipient extends PureComponent { renderRecipientWithAddress () { const { t } = this.context - const { recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props + const { recipientEns, recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props const checksummedRecipientAddress = checksumAddress(recipientAddress) return ( @@ -107,7 +122,18 @@ export default class SenderToRecipient extends PureComponent { { this.renderRecipientIdenticon() } {t('copiedExclamation')}

+ : (addressOnly && !recipientNickname && !recipientEns) + ?

{t('copyAddress')}

+ : ( +

+ {addressSlicer(checksummedRecipientAddress)}
+ {t('copyAddress')} +

+ ) + } wrapperClassName="sender-to-recipient__tooltip-wrapper" containerClassName="sender-to-recipient__tooltip-container" onHidden={() => this.setState({ recipientAddressCopied: false })} @@ -116,8 +142,8 @@ export default class SenderToRecipient extends PureComponent { { addressOnly ? `${t('to')}: ` : '' } { addressOnly - ? checksummedRecipientAddress - : (recipientNickname || recipientName || this.context.t('newContract')) + ? (recipientNickname || recipientEns || checksummedRecipientAddress) + : (recipientNickname || recipientEns || recipientName || this.context.t('newContract')) }
diff --git a/ui/app/components/ui/tooltip-v2.js b/ui/app/components/ui/tooltip-v2.js index b54026794..8d63e1515 100644 --- a/ui/app/components/ui/tooltip-v2.js +++ b/ui/app/components/ui/tooltip-v2.js @@ -8,6 +8,7 @@ export default class Tooltip extends PureComponent { children: null, containerClassName: '', hideOnClick: false, + html: null, onHidden: null, position: 'left', size: 'small', @@ -21,6 +22,7 @@ export default class Tooltip extends PureComponent { children: PropTypes.node, containerClassName: PropTypes.string, disabled: PropTypes.bool, + html: PropTypes.node, onHidden: PropTypes.func, position: PropTypes.oneOf([ 'top', @@ -38,9 +40,9 @@ export default class Tooltip extends PureComponent { } render () { - const {arrow, children, containerClassName, disabled, position, size, title, trigger, onHidden, wrapperClassName, style } = this.props + const {arrow, children, containerClassName, disabled, position, html, size, title, trigger, onHidden, wrapperClassName, style } = this.props - if (!title) { + if (!title && !html) { return (
{children} @@ -51,6 +53,7 @@ export default class Tooltip extends PureComponent { return (
{ const isMainnet = getIsMainnet(state) const { confirmTransaction, metamask } = state const { + ensResolutionsByAddress, conversionRate, identities, addressBook, @@ -93,7 +95,9 @@ const mapStateToProps = (state, ownProps) => { : addressSlicer(checksumAddress(toAddress)) ) - const addressBookObject = addressBook[checksumAddress(toAddress)] + const checksummedAddress = checksumAddress(toAddress) + const addressBookObject = addressBook[checksummedAddress] + const toEns = ensResolutionsByAddress[checksummedAddress] || '' const toNickname = addressBookObject ? addressBookObject.name : '' const isTxReprice = Boolean(lastGasPrice) const transactionStatus = transaction ? transaction.status : '' @@ -134,6 +138,7 @@ const mapStateToProps = (state, ownProps) => { fromAddress, fromName, toAddress, + toEns, toName, toNickname, ethTransactionAmount, @@ -176,6 +181,9 @@ const mapStateToProps = (state, ownProps) => { export const mapDispatchToProps = dispatch => { return { + tryReverseResolveAddress: (address) => { + return dispatch(tryReverseResolveAddress(address)) + }, updateCustomNonce: value => { customNonceValue = value dispatch(updateCustomNonce(value)) diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 59bad34bf..f76024590 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -392,6 +392,8 @@ var actions = { setShowRestorePromptToFalse, turnThreeBoxSyncingOn, turnThreeBoxSyncingOnAndInitialize, + + tryReverseResolveAddress, } module.exports = actions @@ -599,6 +601,19 @@ function requestRevealSeedWords (password) { } } +function tryReverseResolveAddress (address) { + return () => { + return new Promise((resolve) => { + background.tryReverseResolveAddress(address, (err) => { + if (err) { + log.error(err) + } + resolve() + }) + }) + } +} + function fetchInfoToSync () { return dispatch => { log.debug(`background.fetchInfoToSync`) diff --git a/yarn.lock b/yarn.lock index d752122bd..774d204e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21366,7 +21366,7 @@ punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== From 57a29668f3caf3716fb99b904a53ec71cbce6ecb Mon Sep 17 00:00:00 2001 From: Terry Smith <52763493+tshfx@users.noreply.github.com> Date: Mon, 4 Nov 2019 08:40:46 -0400 Subject: [PATCH 10/28] New signature request v3 UI (#6891) * Refactoring signature-request out to a new component. Wip * Styling polish and a better message display. * Update signature request header to no longer use account dropdown mini * Clean up code and styles * Code cleanup for signature request redesign branch * Fix signature request design for full screen * Replace makenode with object.entries in signature-request-message.component.js * Remove unused accounts prop from signature-request.component.js * Use beforeunload instead of window.onbeforeunload in signature-request --- app/_locales/en/messages.json | 3 + ui/app/components/app/index.scss | 2 +- ...quest.js => signature-request-original.js} | 22 ++--- .../components/app/signature-request/index.js | 1 + .../app/signature-request/index.scss | 96 +++++++++++++++++++ .../signature-request-footer/index.js | 1 + .../signature-request-footer/index.scss | 18 ++++ .../signature-request-footer.component.js | 24 +++++ .../signature-request-header/index.js | 1 + .../signature-request-header/index.scss | 25 +++++ .../signature-request-header.component.js | 29 ++++++ .../signature-request-message/index.js | 1 + .../signature-request-message/index.scss | 67 +++++++++++++ .../signature-request-message.component.js | 50 ++++++++++ .../signature-request.component.js | 81 ++++++++++++++++ .../signature-request.constants.js | 3 + .../signature-request.container.js | 72 ++++++++++++++ .../tests/signature-request.test.js | 25 +++++ ui/app/pages/confirm-transaction/conf-tx.js | 50 ++++++---- .../confirm-transaction.component.js | 2 +- .../confirm-transaction.container.js | 2 + 21 files changed, 542 insertions(+), 33 deletions(-) rename ui/app/components/app/{signature-request.js => signature-request-original.js} (94%) create mode 100644 ui/app/components/app/signature-request/index.js create mode 100644 ui/app/components/app/signature-request/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-footer/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-footer/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js create mode 100644 ui/app/components/app/signature-request/signature-request-header/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-header/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js create mode 100644 ui/app/components/app/signature-request/signature-request-message/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-message/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js create mode 100644 ui/app/components/app/signature-request/signature-request.component.js create mode 100644 ui/app/components/app/signature-request/signature-request.constants.js create mode 100644 ui/app/components/app/signature-request/signature-request.container.js create mode 100644 ui/app/components/app/signature-request/tests/signature-request.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3733830cc..d4c8caffa 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1188,6 +1188,9 @@ "signatureRequest": { "message": "Signature Request" }, + "signatureRequest1": { + "message": "Message" + }, "signed": { "message": "Signed" }, diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index 1ccb6a94a..d1ffa4949 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -84,4 +84,4 @@ @import 'home-notification/index'; -@import 'multiple-notifications/index'; +@import 'signature-request/index'; diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request-original.js similarity index 94% rename from ui/app/components/app/signature-request.js rename to ui/app/components/app/signature-request-original.js index e7370c124..0a9a43593 100644 --- a/ui/app/components/app/signature-request.js +++ b/ui/app/components/app/signature-request-original.js @@ -243,7 +243,7 @@ SignatureRequest.prototype.renderBody = function () { let notice = this.context.t('youSign') + ':' const { txData } = this.props - const { type, msgParams: { data, version } } = txData + const { type, msgParams: { data } } = txData if (type === 'personal_sign') { rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }] @@ -275,17 +275,15 @@ SignatureRequest.prototype.renderBody = function () { }, [notice]), h('div.request-signature__rows', - type === 'eth_signTypedData' && (version === 'V3' || version === 'V4') ? - this.renderTypedData(data) : - rows.map(({ name, value }) => { - if (typeof value === 'boolean') { - value = value.toString() - } - return h('div.request-signature__row', [ - h('div.request-signature__row-title', [`${name}:`]), - h('div.request-signature__row-value', value), - ]) - }), + rows.map(({ name, value }, index) => { + if (typeof value === 'boolean') { + value = value.toString() + } + return h('div.request-signature__row', { key: `request-signature-row-${index}` }, [ + h('div.request-signature__row-title', [`${name}:`]), + h('div.request-signature__row-value', value), + ]) + }) ), ]) } diff --git a/ui/app/components/app/signature-request/index.js b/ui/app/components/app/signature-request/index.js new file mode 100644 index 000000000..b1c8a1960 --- /dev/null +++ b/ui/app/components/app/signature-request/index.js @@ -0,0 +1 @@ +export { default } from './signature-request.container' diff --git a/ui/app/components/app/signature-request/index.scss b/ui/app/components/app/signature-request/index.scss new file mode 100644 index 000000000..69115681f --- /dev/null +++ b/ui/app/components/app/signature-request/index.scss @@ -0,0 +1,96 @@ +@import 'signature-request-footer/index'; +@import 'signature-request-header/index'; +@import 'signature-request-message/index'; + +.signature-request { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + + @media screen and (min-width: 576px) { + flex: initial; + } +} + +.signature-request-header { + flex: 1; + + .network-display__container { + padding: 0; + justify-content: flex-end; + } + + .network-display__name { + font-size: 12px; + white-space: nowrap; + font-weight: 500; + } +} + +.signature-request-content { + flex: 1 40%; + margin-top: 1rem; + display: flex; + align-items: center; + flex-direction: column; + margin-bottom: 25px; + min-height: min-content; + + &__title { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-size: 18px; + } + + &__identicon-container { + padding: 1rem; + flex: 1; + position: relative; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + &__identicon-border { + height: 75px; + width: 75px; + border-radius: 50%; + border: 1px solid white; + position: absolute; + box-shadow: 0 2px 2px 0.5px rgba(0, 0, 0, 0.19); + } + + &__identicon-initial { + position: absolute; + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-size: 60px; + color: white; + z-index: 1; + text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.422); + } + + &__info { + font-size: 12px; + } + + &__info--bolded { + font-size: 16px; + font-weight: 500; + } + + p { + color: #999999; + font-size: 0.8rem; + } + + .identicon {} +} + +.signature-request-footer { + flex: 1 1 auto; +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-footer/index.js b/ui/app/components/app/signature-request/signature-request-footer/index.js new file mode 100644 index 000000000..11d0b3944 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-footer.component' diff --git a/ui/app/components/app/signature-request/signature-request-footer/index.scss b/ui/app/components/app/signature-request/signature-request-footer/index.scss new file mode 100644 index 000000000..d8c6b36d6 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/index.scss @@ -0,0 +1,18 @@ +.signature-request-footer { + display: flex; + border-top: 1px solid #d2d8dd; + + button { + text-transform: uppercase; + flex: 1; + margin: 1rem 0.5rem; + border-radius: 3px; + } + + button:first-child() { + margin-left: 1rem; + } + button:last-child() { + margin-right: 1rem; + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js b/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js new file mode 100644 index 000000000..591b9a03a --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js @@ -0,0 +1,24 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Button from '../../../ui/button' + +export default class SignatureRequestFooter extends PureComponent { + static propTypes = { + cancelAction: PropTypes.func.isRequired, + signAction: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { cancelAction, signAction } = this.props + return ( +
+ + +
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request-header/index.js b/ui/app/components/app/signature-request/signature-request-header/index.js new file mode 100644 index 000000000..fa596383a --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-header.component' diff --git a/ui/app/components/app/signature-request/signature-request-header/index.scss b/ui/app/components/app/signature-request/signature-request-header/index.scss new file mode 100644 index 000000000..7a33f85f2 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/index.scss @@ -0,0 +1,25 @@ +.signature-request-header { + display: flex; + padding: 1rem; + border-bottom: 1px solid $geyser; + justify-content: space-between; + font-size: .75rem; + + &--account, &--network { + flex: 1; + } + + &--account { + display: flex; + align-items: center; + + .account-list-item__account-name { + font-size: 12px; + font-weight: 500; + } + + .account-list-item__top-row { + margin: 0px; + } + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js b/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js new file mode 100644 index 000000000..3ac0c9afb --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js @@ -0,0 +1,29 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component' +import NetworkDisplay from '../../network-display' + +export default class SignatureRequestHeader extends PureComponent { + static propTypes = { + selectedAccount: PropTypes.object.isRequired, + } + + render () { + const { selectedAccount } = this.props + + return ( +
+
+ {selectedAccount && } + {name} +
+
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request-message/index.js b/ui/app/components/app/signature-request/signature-request-message/index.js new file mode 100644 index 000000000..e62265a5f --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-message.component' diff --git a/ui/app/components/app/signature-request/signature-request-message/index.scss b/ui/app/components/app/signature-request/signature-request-message/index.scss new file mode 100644 index 000000000..aec597f89 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/index.scss @@ -0,0 +1,67 @@ +.signature-request-message { + flex: 1 60%; + display: flex; + flex-direction: column; + + &__title { + font-weight: 500; + font-size: 14px; + color: #636778; + margin-left: 12px; + } + + h2 { + flex: 1 1 0; + text-align: left; + font-size: 0.8rem; + border-bottom: 1px solid #d2d8dd; + padding: 0.5rem; + margin: 0; + color: #ccc; + } + + &--root { + flex: 1 100%; + background-color: #f8f9fb; + padding-bottom: 0.5rem; + overflow: auto; + padding-left: 12px; + padding-right: 12px; + width: 360px; + font-family: monospace; + + @media screen and (min-width: 576px) { + width: auto; + } + } + + &__type-title { + font-family: monospace; + font-style: normal; + font-weight: normal; + font-size: 14px; + margin-left: 12px; + margin-top: 6px; + margin-bottom: 10px; + } + + &--node, &--node-leaf { + padding-left: 0.8rem; + + &-label { + color: #5B5D67; + } + + &-value { + color: black; + margin-left: 0.5rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + + &--node-leaf { + display: flex; + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js new file mode 100644 index 000000000..16b6c3bea --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js @@ -0,0 +1,50 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class SignatureRequestMessage extends PureComponent { + static propTypes = { + data: PropTypes.object.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + renderNode (data) { + return ( +
+ {Object.entries(data).map(([ label, value ], i) => ( +
+ {label}: + { + typeof value === 'object' && value !== null ? + this.renderNode(value) + : {value} + } +
+ ))} +
+ ) + } + + + render () { + const { data } = this.props + + return ( +
+
{this.context.t('signatureRequest1')}
+
+
{this.context.t('signatureRequest1')}
+ {this.renderNode(data)} +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request.component.js b/ui/app/components/app/signature-request/signature-request.component.js new file mode 100644 index 000000000..7029b1e00 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.component.js @@ -0,0 +1,81 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Header from './signature-request-header' +import Footer from './signature-request-footer' +import Message from './signature-request-message' +import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import Identicon from '../../ui/identicon' + +export default class SignatureRequest extends PureComponent { + static propTypes = { + txData: PropTypes.object.isRequired, + selectedAccount: PropTypes.shape({ + address: PropTypes.string, + balance: PropTypes.string, + name: PropTypes.string, + }).isRequired, + + clearConfirmTransaction: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + sign: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + componentDidMount () { + const { clearConfirmTransaction, cancel } = this.props + const { metricsEvent } = this.context + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.addEventListener('beforeunload', (event) => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel Sig Request Via Notification Close', + }, + }) + clearConfirmTransaction() + cancel(event) + }) + } + } + + formatWallet (wallet) { + return `${wallet.slice(0, 8)}...${wallet.slice(wallet.length - 8, wallet.length)}` + } + + render () { + const { + selectedAccount, + txData: { msgParams: { data, origin, from: senderWallet }}, + cancel, + sign, + } = this.props + const { message } = JSON.parse(data) + + return ( +
+
+
+
{this.context.t('sigRequest')}
+
+
{ message.from.name && message.from.name[0] }
+
+ +
+
{message.from.name}
+
{origin}
+
{this.formatWallet(senderWallet)}
+
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request.constants.js b/ui/app/components/app/signature-request/signature-request.constants.js new file mode 100644 index 000000000..9cf241928 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.constants.js @@ -0,0 +1,3 @@ +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' + +export { ENVIRONMENT_TYPE_NOTIFICATION } diff --git a/ui/app/components/app/signature-request/signature-request.container.js b/ui/app/components/app/signature-request/signature-request.container.js new file mode 100644 index 000000000..0b09c1a64 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.container.js @@ -0,0 +1,72 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import SignatureRequest from './signature-request.component' +import { goHome } from '../../../store/actions' +import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck' +import { + getSelectedAccount, + getCurrentAccountWithSendEtherInfo, + getSelectedAddress, + accountsWithSendEtherInfoSelector, + conversionRateSelector, +} from '../../../selectors/selectors.js' + +function mapStateToProps (state) { + return { + balance: getSelectedAccount(state).balance, + selectedAccount: getCurrentAccountWithSendEtherInfo(state), + selectedAddress: getSelectedAddress(state), + accounts: accountsWithSendEtherInfoSelector(state), + conversionRate: conversionRateSelector(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(goHome()), + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + } +} + +function mergeProps (stateProps, dispatchProps, ownProps) { + const { + signPersonalMessage, + signTypedMessage, + cancelPersonalMessage, + cancelTypedMessage, + signMessage, + cancelMessage, + txData, + } = ownProps + + const { type } = txData + + let cancel + let sign + + if (type === 'personal_sign') { + cancel = cancelPersonalMessage + sign = signPersonalMessage + } else if (type === 'eth_signTypedData') { + cancel = cancelTypedMessage + sign = signTypedMessage + } else if (type === 'eth_sign') { + cancel = cancelMessage + sign = signMessage + } + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + txData, + cancel, + sign, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(SignatureRequest) diff --git a/ui/app/components/app/signature-request/tests/signature-request.test.js b/ui/app/components/app/signature-request/tests/signature-request.test.js new file mode 100644 index 000000000..68b114dd8 --- /dev/null +++ b/ui/app/components/app/signature-request/tests/signature-request.test.js @@ -0,0 +1,25 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../lib/shallow-with-context' +import SignatureRequest from '../signature-request.component' + + +describe('Signature Request Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + describe('render', () => { + it('should render a div with one child', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.length, 1) + assert(wrapper.hasClass('signature-request')) + }) + }) +}) diff --git a/ui/app/pages/confirm-transaction/conf-tx.js b/ui/app/pages/confirm-transaction/conf-tx.js index 4f3868bc8..ce1edde5c 100644 --- a/ui/app/pages/confirm-transaction/conf-tx.js +++ b/ui/app/pages/confirm-transaction/conf-tx.js @@ -9,7 +9,8 @@ const txHelper = require('../../../lib/tx-helper') const log = require('loglevel') const R = require('ramda') -const SignatureRequest = require('../../components/app/signature-request') +const SignatureRequest = require('../../components/app/signature-request').default +const SignatureRequestOriginal = require('../../components/app/signature-request-original') const Loading = require('../../components/ui/loading-screen') const { DEFAULT_ROUTE } = require('../../helpers/constants/routes') @@ -137,34 +138,45 @@ ConfirmTxScreen.prototype.getTxData = function () { : unconfTxList[index] } +ConfirmTxScreen.prototype.signatureSelect = function (type, version) { + // Temporarily direct only v3 and v4 requests to new code. + if (type === 'eth_signTypedData' && (version === 'V3' || version === 'V4')) { + return SignatureRequest + } + + return SignatureRequestOriginal +} + ConfirmTxScreen.prototype.render = function () { const props = this.props const { currentCurrency, blockGasLimit, + conversionRate, } = props var txData = this.getTxData() || {} - const { msgParams } = txData + const { msgParams, type, msgParams: { version } } = txData log.debug('msgParams detected, rendering pending msg') - return msgParams - ? h(SignatureRequest, { - // Properties - txData: txData, - key: txData.id, - identities: props.identities, - currentCurrency, - blockGasLimit, - // Actions - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - signTypedMessage: this.signTypedMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), - }) - : h(Loading) + return msgParams ? h(this.signatureSelect(type, version), { + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + signTypedMessage: this.signTypedMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), + }) : h(Loading) } ConfirmTxScreen.prototype.signMessage = function (msgData, event) { diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/pages/confirm-transaction/confirm-transaction.component.js index 4b37cf2b1..9cb69e0da 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.component.js @@ -45,6 +45,7 @@ export default class ConfirmTransaction extends Component { isTokenMethodAction: PropTypes.bool, fullScreenVsPopupTestGroup: PropTypes.string, trackABTest: PropTypes.bool, + conversionRate: PropTypes.number, } componentDidMount () { @@ -118,7 +119,6 @@ export default class ConfirmTransaction extends Component { // Show routes when state.confirmTransaction has been set and when either the ID in the params // isn't specified or is specified and matches the ID in state.confirmTransaction in order to // support URLs of /confirm-transaction or /confirm-transaction/ - return transactionId && (!paramsTransactionId || paramsTransactionId === transactionId) ? ( diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/pages/confirm-transaction/confirm-transaction.container.js index 9625db8ec..7c3986441 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.container.js @@ -25,6 +25,7 @@ const mapStateToProps = (state, ownProps) => { send, unapprovedTxs, abTests: { fullScreenVsPopup }, + conversionRate, }, confirmTransaction, } = state @@ -53,6 +54,7 @@ const mapStateToProps = (state, ownProps) => { isTokenMethodAction: isTokenMethodAction(transactionCategory), trackABTest, fullScreenVsPopupTestGroup: fullScreenVsPopup, + conversionRate, } } From 6a4df0dc3f620ef0bfe4c1255c1f791390b4280a Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 4 Nov 2019 11:13:24 -0330 Subject: [PATCH 11/28] Adds Wyre Widget (#6434) * Adds Wyre widget to the deposit modal. * Move wyre widget code to vendor directory * Get Wyre widget working without metamask connect/sign steps * Code cleanup for wyre changes * Change wyre widget to using prod environment * Remove code allowing signing of wyre messages without confirmations * Update wyre vendor code for wyre 2.0 * Remove unnecessary changes to provider approval constructor, triggerUI and openPopup * Fix Wyre translation message * Delete no longer used signature-request-modal * Fix documentation of matches function in utils/util.js * Code cleanup on wyre branch * Remove front end code changes not needed to support wyre v2 --- app/_locales/en/messages.json | 2 +- .../app/modals/deposit-ether-modal.js | 16 +++- ui/app/helpers/constants/routes.js | 2 + ui/app/pages/routes/index.js | 22 +++++ ui/vendor/wyre.js | 87 +++++++++++++++++++ 5 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 ui/vendor/wyre.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index d4c8caffa..c6b0063e4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -214,7 +214,7 @@ "message": "Buy ETH with Wyre" }, "buyWithWyreDescription": { - "message": "Wyre lets you use a credit card to deposit ETH right in to your MetaMask account." + "message": "Wyre lets you use a credit card to deposit ETH right in to your MetaMask account. Open Wyre's widget here to get started." }, "buyCoinSwitch": { "message": "Buy on CoinSwitch" diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index f71e0619e..9d8fcffd5 100644 --- a/ui/app/components/app/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -5,6 +5,10 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../../store/actions') const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util') +const openWyre = require('../../../../vendor/wyre') +const { DEPOSIT_ROUTE } = require('../../../helpers/constants/routes') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums') +const { getEnvironmentType } = require('../../../../../app/scripts/lib/util') import Button from '../../ui/button' @@ -121,7 +125,7 @@ DepositEtherModal.prototype.renderRow = function ({ } DepositEtherModal.prototype.render = function () { - const { network, toWyre, toCoinSwitch, address, toFaucet } = this.props + const { network, address, toFaucet, toCoinSwitch } = this.props const isTestNetwork = ['3', '4', '5', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) @@ -182,8 +186,14 @@ DepositEtherModal.prototype.render = function () { title: WYRE_ROW_TITLE, text: WYRE_ROW_TEXT, buttonLabel: this.context.t('continueToWyre'), - onButtonClick: () => toWyre(address), - hide: isTestNetwork, + onButtonClick: () => { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser(DEPOSIT_ROUTE) + } else { + openWyre(address) + } + }, + hide: isTestNetwork && !network === '42', }), this.renderRow({ diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 8c2a8b8bc..44e05ce75 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -25,6 +25,7 @@ const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' const CONNECT_HARDWARE_ROUTE = '/new-account/connect' const SEND_ROUTE = '/send' +const DEPOSIT_ROUTE = '/deposit' const INITIALIZE_ROUTE = '/initialize' const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome' @@ -94,4 +95,5 @@ module.exports = { CONTACT_MY_ACCOUNTS_EDIT_ROUTE, NETWORKS_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, + DEPOSIT_ROUTE, } diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index 01e61b1b4..30db8be7c 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -8,6 +8,7 @@ import log from 'loglevel' import IdleTimer from 'react-idle-timer' import {getNetworkIdentifier, preferencesSelector} from '../../selectors/selectors' import classnames from 'classnames' +import openWyre from '../../../vendor/wyre' // init import FirstTimeFlow from '../first-time-flow' @@ -67,6 +68,7 @@ import { CONFIRM_TRANSACTION_ROUTE, INITIALIZE_ROUTE, INITIALIZE_UNLOCK_ROUTE, + DEPOSIT_ROUTE, } from '../../helpers/constants/routes' // enums @@ -98,6 +100,20 @@ class Routes extends Component { }) } + componentDidMount () { + const { + location, + modal, + showDepositModal, + selectedAddress, + } = this.props + + if (location.pathname === DEPOSIT_ROUTE && (!modal || !modal.open) && selectedAddress) { + showDepositModal() + openWyre(selectedAddress) + } + } + renderRoutes () { const { autoLogoutTimeLimit, setLastActiveTime } = this.props @@ -116,6 +132,7 @@ class Routes extends Component { + ) @@ -348,6 +365,9 @@ Routes.propTypes = { providerId: PropTypes.string, providerRequests: PropTypes.array, autoLogoutTimeLimit: PropTypes.number, + showDepositModal: PropTypes.func, + modal: PropTypes.object, + selectedAddress: PropTypes.string, } function mapStateToProps (state) { @@ -381,6 +401,7 @@ function mapStateToProps (state) { providerId: getNetworkIdentifier(state), autoLogoutTimeLimit, providerRequests: metamask.providerRequests, + selectedAddress: metamask.selectedAddress, } } @@ -391,6 +412,7 @@ function mapDispatchToProps (dispatch) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), setLastActiveTime: () => dispatch(actions.setLastActiveTime()), + showDepositModal: () => dispatch(actions.showModal({ name: 'DEPOSIT_ETHER' })), } } diff --git a/ui/vendor/wyre.js b/ui/vendor/wyre.js new file mode 100644 index 000000000..7793b935c --- /dev/null +++ b/ui/vendor/wyre.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +// code taken from https://verify.sendwyre.com/js/verify-module-init-beta.js +'use strict' + +function _classCallCheck (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError('Cannot call a class as a function') + } +} + +function _defineProperties (target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i] + descriptor.enumerable = descriptor.enumerable || false + descriptor.configurable = true + if ('value' in descriptor) descriptor.writable = true + Object.defineProperty(target, descriptor.key, descriptor) + } +} + +function _createClass (Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps) + if (staticProps) _defineProperties(Constructor, staticProps) + return Constructor +} + +function createWyreWidget () { + var ga = null + var Widget = function () { + function a (b) { + var c = !!(1 < arguments.length && void 0 !== arguments[1]) && arguments[1]; + _classCallCheck(this,a), this.debug = c, this.operationHostedWidget="debitcard-hosted", this.operationHostedDialogWidget="debitcard-hosted-dialog", this.operationWidgetLite="debitcard-whitelabel",this.operationDigitalWallet="debitcard",this.operationAch="onramp",this.validOperations=[this.operationWidgetLite,this.operationDigitalWallet,this.operationHostedWidget,this.operationAch,this.operationHostedDialogWidget],this.injectedWidgetOperations=[this.operationWidgetLite,this.operationDigitalWallet],this.iframeWidgetOperations=[this.operationAch],this.queue=[],this.ready=!1,this.eventRegistrations=new Map,null==b.env&&(b.env="production"),this.initParams=b,this.init=this.processClassicInit(b); + var d=b; + console.log("init",b,d),this.getNewWidgetScriptLocation(),this.operationType=d.operation&&d.operation.type||"",this.validateOperationType(),this.injectedWidgetOperations.includes(this.operationType)?(console.log("inject run"),this.attachEvents(),this.createInjectedWidget()):this.iframeWidgetOperations.includes(this.operationType)?(console.log("iframe run"),this.validateInit(),this.attachEvents(),this.createIframe()):this.operationHostedWidget===this.operationType?this.handleHostedWidget():this.operationHostedDialogWidget===this.operationType&&this.emit("ready"); + } + return _createClass( + a, + [ + {key:"validateOperationType",value:function a(){if(!this.validOperations.includes(this.operationType)){var b="supplied operation type >>"+this.operationType+"<< is invalid, valid types are:"+this.validOperations.join(",").toString();throw this.emit("close",{error:b}),this.removeClass(),new Error(b)}}}, + {key:"removeListener",value:function d(a,b){var c=this.eventRegistrations.get(a)||[];c=c.filter(function(a){return a!==b}),this.eventRegistrations.set(a,c);}}, + {key:"removeAllListeners",value:function b(a){a?this.eventRegistrations.set(a,[]):this.eventRegistrations=new Map;}}, + {key:"on",value:function d(a,b){if(!a)throw new Error("must supply an event!");var c=this.eventRegistrations.get(a)||[];c.push(b),this.eventRegistrations.set(a,c);}}, + {key:"open",value:function a(){return this.send("open",{}),console.log(this.operationType),this.operationType==this.operationHostedDialogWidget?void this.handleHostedDialogWidget():void(this.iframe?this.iframe.style.display="block":this.injectedWidget&&(this.injectedWidget.style.display="block"))}}, + {key:"emit",value:function d(a,b){var c=this.eventRegistrations.get(a)||[];c.forEach(function(a){try{a(b||{});}catch(a){console.warn("subscribed widget event handler failure: ",a);}});}}, + {key:"validateInit",value:function a(){switch(this.init.auth.type){case"secretKey":if(this.init.error)return;var b=this.init.auth.secretKey;if(25>b.length)return console.error("Diligently refusing to accept a secret key with length < 25"),this.emit("close",{error:"supplied secretKey is too short"}),void this.removeClass();}}}, + {key:"send",value:function c(a,b){this.queue.push({type:a,payload:b}),this.flush();}}, + {key:"flush",value:function b(){var a=this;this.ready&&(this.queue.forEach(function(b){return a.iframe.contentWindow.postMessage(JSON.stringify(b),"*")}),this.queue=[]);}}, + {key:"attachEvents",value:function d(){var a=this,b=window.addEventListener?"addEventListener":"attachEvent",c="attachEvent"==b?"onmessage":"message";window[b](c,function(b){if("string"==typeof b.data&&0==b.data.indexOf("{")){var c=JSON.parse(b.data);if(console.log("frame",c),!!c.type)switch(c.type){case"ready":a.ready=!0,a.init.web3PresentInParentButNotChild=c.payload&&!c.payload.web3Enabled&&"undefined"!=typeof web3,"function"==typeof ga&&c.payload&&c.payload.gaTrackingCode?(ga("create",c.payload.gaTrackingCode,"auto"),ga(function(b){var c=b.get("clientId");a.send("init",a.init),a.emitReady();})):(a.send("init",a.init),a.emitReady());break;case"close":case"complete":a.close(),a.removeClass(),a.emit(c.type,c.payload);break;case"sign-request":var d=c.payload,e=new Web3(web3.currentProvider);e.personal.sign(e.fromUtf8(d.message),d.address,function(b,c){a.send("sign-response",{signature:c,error:b});});break;case"provider-name":var h=a.getNameOfProvider();a.send("provider-name",h);break;case"accounts":var f=new Web3(web3.currentProvider),g=f.eth.accounts;a.send("accounts-response",{accounts:g});break;default:}}},!1);}}, + {key:"close",value:function a(){this.removeClass(),this.iframe&&document.body.removeChild(this.iframe),this.injectedWidget&&document.body.removeChild(this.injectedWidget),this.injectedWidget=null,this.iframe=null,this.queue=[],this.ready=!1;}}, + {key:"createIframe",value:function b(){var a=Math.round;this.iframe=document.createElement("iframe"),this.iframe.setAttribute("allow","camera;"),this.iframe.style.display="none",this.iframe.style.border="none",this.iframe.style.width="100%",this.iframe.style.height="100%",this.iframe.style.position="fixed",this.iframe.style.zIndex="999999",this.iframe.style.top="0",this.iframe.style.left="0",this.iframe.style.bottom="0",this.iframe.style.right="0",this.iframe.style.backgroundColor="transparent",this.iframe.src=this.getBaseUrl()+"/loader?_cb="+a(new Date().getTime()/1e3),document.body.appendChild(this.iframe);}}, + {key:"getBaseUrl",value:function a(){switch(this.init.env){case"test":return "https://verify.testwyre.com";case"staging":return "https://verify-staging.i.sendwyre.com";case"local":return "http://localhost:8890";case"local_https":return "https://localhost:8890";case"android_emulator":return "http://10.0.2.2:8890";case"production":default:return "https://verify.sendwyre.com";}}}, + {key:"processClassicInit",value:function d(a){if(a.auth)return a;var b=a,c={env:b.env,auth:{type:"metamask"},operation:{type:"onramp",destCurrency:b.destCurrency},apiKey:b.apiKey,web3PresentInParentButNotChild:!1};return b.onExit&&this.on("close",function(a){a.error?(b.onExit(a.error),this.removeClass()):(this.removeClass(),b.onExit(null));}),b.onSuccess&&(this.on("complete",function(){b.onSuccess();}),this.removeClass()),console.debug("converted v1 config to v2, please use this instead: ",c),c}}, + {key:"getNameOfProvider",value:function a(){return web3.currentProvider.isTrust?"trust":"undefined"==typeof __CIPHER__?"undefined"==typeof SOFA?web3.currentProvider.isDDEXWallet?"ddex":"metamask":"coinbase":"cipher"}}, + {key:"getOperationParametersAsQueryString",value:function b(){var a=this;return this.initParams.operation?Object.keys(this.initParams.operation).map(function(b){return b+"="+encodeURIComponent(a.initParams.operation[b])}).join("&"):""}}, + {key:"handleHostedWidget",value:function a(){location.href=this.getPayWidgetLocation()+"/purchase?"+this.getOperationParametersAsQueryString();}}, + {key:"handleHostedDialogWidget",value:function f(){var a=this,b=this.getPayWidgetLocation()+"/purchase?"+this.getOperationParametersAsQueryString(),c=this.openAndCenterDialog(b,360,650),d=window.addEventListener?"addEventListener":"attachEvent",e="attachEvent"===d?"onmessage":"message";window[d](e,function(b){"paymentSuccess"===b.data.type&&a.emit("paymentSuccess",{data:b.data});},!1);}}, + {key:"openAndCenterDialog",value:function o(a,b,c){var d=navigator.userAgent,e=function(){return /\b(iPhone|iP[ao]d)/.test(d)||/\b(iP[ao]d)/.test(d)||/Android/i.test(d)||/Mobile/i.test(d)},f="undefined"==typeof window.screenX?window.screenLeft:window.screenX,g="undefined"==typeof window.screenY?window.screenTop:window.screenY,h="undefined"==typeof window.outerWidth?document.documentElement.clientWidth:window.outerWidth,i="undefined"==typeof window.outerHeight?document.documentElement.clientHeight-22:window.outerHeight,j=e()?null:b,k=e()?null:c,l=0>f?window.screen.width+f:f,m=[];null!==j&&m.push("width="+j),null!==k&&m.push("height="+k),m.push("left="+(l+(h-j)/2)),m.push("top="+(g+(i-k)/2.5)),m.push("scrollbars=1");var n=window.open(a,"Wyre",m.join(","));return window.focus&&n.focus(),n}}, + {key:"getPayWidgetLocation",value:function a(){switch(this.initParams.env){case"test":return "https://pay.testwyre.com";case"staging":return "https://pay-staging.i.sendwyre.com";case"local":return "http://localhost:3000";case"production":default:return "https://pay.sendwyre.com";}}}, + {key:"getNewWidgetScriptLocation",value:function a(){return this.operationType===this.operationWidgetLite?this.getPayWidgetLocation()+"/widget-lite/static/js/widget-lite.js":this.getPayWidgetLocation()+"/digital-wallet-embed.js"}}, + {key:"createInjectedWidget",value:function a(){switch(this.operationType){case"debitcard":{if(this.injectedWidget=document.getElementById("wyre-dropin-widget-container"),!this.injectedWidget){throw this.emit("close",{error:"unable to mount the widget did with id `wyre-dropin-widget-container` not found"}),this.removeClass(),new Error("unable to mount the widget did with id `wyre-dropin-widget-container` not found")}this.injectedWidget.style.display="none",this.handleDebitcardInjection();break}case"debitcard-whitelabel":{if(this.injectedWidget=document.getElementById("wyre-dropin-widget-container"),!this.injectedWidget){throw this.emit("close",{error:"unable to mount the widget did with id `wyre-dropin-widget-container` not found"}),this.removeClass(),new Error("unable to mount the widget did with id `wyre-dropin-widget-container` not found")}this.injectedWidget.style.display="none",this.handleDebitcardWhiteLabelInjection();break}}}}, + {key:"loadJSFile",value:function b(a){return new Promise(function(b,c){var d=document.createElement("script");d.type="text/javascript",d.src=a,d.onload=b,d.onerror=c,d.async=!0,document.getElementsByTagName("head")[0].appendChild(d);})}}, + {key:"handleDebitcardInjection",value:function b(){var a=this;this.loadJSFile(this.getNewWidgetScriptLocation()).then(function(){DigitalWallet.init("wyre-dropin-widget-container",{type:a.operationType,dest:a.initParams.operation.dest,destCurrency:a.initParams.operation.destCurrency,sourceAmount:a.initParams.operation.sourceAmount,paymentMethod:a.initParams.operation.paymentMethod,accountId:a.initParams.accountId},function(b){a.emit("complete",b),a.removeClass();},function(b){a.emit("close",{error:b}),a.removeClass();}),a.send("init",a.init),a.emitReady();}).catch(function(b){throw a.emit("close",{error:b}),a.removeClass(),new Error(b)});}}, + {key:"handleDebitcardWhiteLabelInjection",value:function b(){var a=this;this.loadJSFile(this.getNewWidgetScriptLocation()).then(function(){Wyre2.setData&&Wyre2.widgetLite.appLoaded&&(Wyre2.setData(a.initParams.operation.paymentMethod,a.initParams.operation.sourceAmount,"USD",a.initParams.operation.destCurrency,a.initParams.operation.dest),Wyre2.registerCallback(function(a){this.send("paymentAuthorized",a);},function(a){this.send("error",a),this.removeClass(),console.log(JSON.stringify(a));}),a.send("init",a.init),a.emitReady());}).catch(function(b){throw a.emit("close",{error:b}),a.removeClass(),new Error(b)});}}, + {key:"updateData",value:function e(a,b,c,d){if(this.operationType==this.operationWidgetLite)Wyre2.setData&&Wyre2.widgetLite.appLoaded&&(Wyre2.setData(a,b,"USD",c,d),this.emitReady());else throw this.removeClass(),new Error("this can only be called for operation type "+this.operationWidgetLite)}}, + {key:"emitReady",value:function a(){this.setClassName(),this.emit("ready");}},{key:"removeComponent",value:function a(){}},{key:"setClassName",value:function d(){var a,b,c;a=document.getElementById("wyre-dropin-widget-container"),b="wyre-dropin-widget-container",c=a.className.split(" "),-1==c.indexOf(b)&&(a.className+=" "+b);}},{key:"removeClass",value:function b(){document.getElementById("wyre-dropin-widget-container").style.display="none";var a=document.getElementById("wyre-dropin-widget-container");a.className=a.className.replace(/\bwyre-dropin-widget-container\b/g,"");}} + ] + ),a + }() + var widget=Object.assign(Widget,{Widget:Widget}); + return widget; +} + +function openWyre(address) { + const Wyre = createWyreWidget() + const widget = new Wyre({ + env: 'prod', + operation: { + type: 'debitcard-hosted-dialog', + destCurrency: 'ETH', + dest: `ethereum:${address}`, + }, + }) + + widget.open() +} + +module.exports = openWyre \ No newline at end of file From dbd14d796c4cc1f0d42fbe58a096c3e41d5590a5 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 4 Nov 2019 14:03:57 -0400 Subject: [PATCH 12/28] Clear `beforeunload` handler after button is pressed (#7346) On the signature request and transaction confirmation notification pages, the closure of the notification UI implies that the request has been rejected. However, this rejection is being submitted even when the window is closed as a result of the user explicitly confirming or rejecting. In practice, I suspect this has no effect because the transaction, after being explicitly confirmed or rejected, has already been moved out of a pending state. But just in case there is some present or future edge case that might be affected, the `beforeunload` handler is now removed once the user has explicitly made a choice. This mistake was introduced recently in #7333 --- .../app/signature-request-original.js | 37 ++++++++------ .../confirm-transaction-base.component.js | 48 +++++++++++-------- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/ui/app/components/app/signature-request-original.js b/ui/app/components/app/signature-request-original.js index 0a9a43593..60b910eb0 100644 --- a/ui/app/components/app/signature-request-original.js +++ b/ui/app/components/app/signature-request-original.js @@ -103,31 +103,36 @@ function SignatureRequest (props) { } } -SignatureRequest.prototype.componentDidMount = function () { +SignatureRequest.prototype._beforeUnload = (event) => { const { clearConfirmTransaction, cancel } = this.props const { metricsEvent } = this.context + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel Sig Request Via Notification Close', + }, + }) + clearConfirmTransaction() + cancel(event) +} + +SignatureRequest.prototype._removeBeforeUnload = () => { if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - this._onBeforeUnload = event => { - metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Sign Request', - name: 'Cancel Sig Request Via Notification Close', - }, - }) - clearConfirmTransaction() - cancel(event) - } - window.addEventListener('beforeunload', this._onBeforeUnload) + window.removeEventListener('beforeunload', this._beforeUnload) } } -SignatureRequest.prototype.componentWillUnmount = function () { +SignatureRequest.prototype.componentDidMount = function () { if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.removeEventListener('beforeunload', this._onBeforeUnload) + window.addEventListener('beforeunload', this._beforeUnload) } } +SignatureRequest.prototype.componentWillUnmount = function () { + this._removeBeforeUnload() +} + SignatureRequest.prototype.renderHeader = function () { return h('div.request-signature__header', [ @@ -297,6 +302,7 @@ SignatureRequest.prototype.renderFooter = function () { large: true, className: 'request-signature__footer__cancel-button', onClick: event => { + this._removeBeforeUnload() cancel(event).then(() => { this.context.metricsEvent({ eventOpts: { @@ -315,6 +321,7 @@ SignatureRequest.prototype.renderFooter = function () { large: true, className: 'request-signature__footer__sign-button', onClick: event => { + this._removeBeforeUnload() sign(event).then(() => { this.context.metricsEvent({ eventOpts: { diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 0e81dfe6e..25725381d 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -387,7 +387,8 @@ export default class ConfirmTransactionBase extends Component { showRejectTransactionsConfirmationModal({ unapprovedTxCount, - async onSubmit () { + onSubmit: async () => { + this._removeBeforeUnload() await cancelAllTransactions() clearConfirmTransaction() history.push(DEFAULT_ROUTE) @@ -409,6 +410,7 @@ export default class ConfirmTransactionBase extends Component { updateCustomNonce, } = this.props + this._removeBeforeUnload() metricsEvent({ eventOpts: { category: 'Transactions', @@ -458,6 +460,7 @@ export default class ConfirmTransactionBase extends Component { submitting: true, submitError: null, }, () => { + this._removeBeforeUnload() metricsEvent({ eventOpts: { category: 'Transactions', @@ -568,8 +571,30 @@ export default class ConfirmTransactionBase extends Component { } } + _beforeUnload = () => { + const { txData: { origin, id } = {}, cancelTransaction } = this.props + const { metricsEvent } = this.context + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Cancel Tx Via Notification Close', + }, + customVariables: { + origin, + }, + }) + cancelTransaction({ id }) + } + + _removeBeforeUnload = () => { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.removeEventListener('beforeunload', this._beforeUnload) + } + } + componentDidMount () { - const { toAddress, txData: { origin, id } = {}, cancelTransaction, getNextNonce, tryReverseResolveAddress } = this.props + const { toAddress, txData: { origin } = {}, getNextNonce, tryReverseResolveAddress } = this.props const { metricsEvent } = this.context metricsEvent({ eventOpts: { @@ -583,20 +608,7 @@ export default class ConfirmTransactionBase extends Component { }) if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - this._onBeforeUnload = () => { - metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Confirm Screen', - name: 'Cancel Tx Via Notification Close', - }, - customVariables: { - origin, - }, - }) - cancelTransaction({ id }) - } - window.addEventListener('beforeunload', this._onBeforeUnload) + window.addEventListener('beforeunload', this._beforeUnload) } getNextNonce() @@ -604,9 +616,7 @@ export default class ConfirmTransactionBase extends Component { } componentWillUnmount () { - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.removeEventListener('beforeunload', this._onBeforeUnload) - } + this._removeBeforeUnload() } render () { From eef570cf952b4be8d430fe25869295851f9fe291 Mon Sep 17 00:00:00 2001 From: hjlee9182 <46337218+hjlee9182@users.noreply.github.com> Date: Tue, 5 Nov 2019 04:08:17 +0900 Subject: [PATCH 13/28] fix width in first time flow button (#7348) --- ui/app/pages/first-time-flow/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/pages/first-time-flow/index.scss b/ui/app/pages/first-time-flow/index.scss index c674551f4..58718c581 100644 --- a/ui/app/pages/first-time-flow/index.scss +++ b/ui/app/pages/first-time-flow/index.scss @@ -120,7 +120,7 @@ &__button { margin: 35px 0 14px; - width: 140px; + width: 170px; height: 44px; } From 99b8f2d5445ba2604516c064757b257774066b4d Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 4 Nov 2019 17:28:50 -0400 Subject: [PATCH 14/28] Fix provider approval metadata (#7349) * Omit MetaMask `extensionId` from site metadata The site metadata was updated in #7218 to include the extension id of the extension connecting to MetaMask. This was done to allow external extensions to connect with MetaMask, so that we could show the id on the provider approval screen. Unbeknownst to me at the time, the extension id was being set for all connections to MetaMask from dapps. The id was set to MetaMask's id, because the connections are made through MetaMask's contentscript. This has been updated to only set the id when accepting a connection from a different extension. * Fix `siteMetadata` property names In #7218 a few things were added to the site metadata, so the provider approval controller was middleware was updated to accept the site metadata as an object rather than accepting each property as a separate parameter. Unfortunately we failed to notice that the site name and icon were named differently in the site metadata than they were in the provider approval controller, so the names of those properties were unintentionally changed in the controller state. The provider approval controller has been updated to restore the original property names of `siteTitle` and `siteIcon`. An unused prop that was added to the provider approval page in #7218 has also been removed. --- app/scripts/background.js | 5 ++++- app/scripts/controllers/provider-approval.js | 3 ++- .../provider-approval/provider-approval.component.js | 9 +++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/scripts/background.js b/app/scripts/background.js index 7828c6d80..7c347d62e 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -391,7 +391,10 @@ function setupController (initState, initLangCode) { // communication with page or other extension function connectExternal (remotePort) { const senderUrl = new URL(remotePort.sender.url) - const extensionId = remotePort.sender.id + let extensionId + if (remotePort.sender.id !== extension.runtime.id) { + extensionId = remotePort.sender.id + } const portStream = new PortStream(remotePort) controller.setupUntrustedCommunication(portStream, senderUrl, extensionId) } diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 3ece07e13..00ff626f7 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -47,7 +47,8 @@ class ProviderApprovalController extends SafeEventEmitter { if (extensionId) { metadata.extensionId = extensionId } else { - Object.assign(metadata, await getSiteMetadata(origin)) + const siteMetadata = await getSiteMetadata(origin) + Object.assign(metadata, { siteTitle: siteMetadata.name, siteImage: siteMetadata.icon}) } this._handleProviderRequest(metadata) // wait for resolution of request diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js index da177defc..8532fe60d 100644 --- a/ui/app/pages/provider-approval/provider-approval.component.js +++ b/ui/app/pages/provider-approval/provider-approval.component.js @@ -6,7 +6,13 @@ export default class ProviderApproval extends Component { static propTypes = { approveProviderRequestByOrigin: PropTypes.func.isRequired, rejectProviderRequestByOrigin: PropTypes.func.isRequired, - providerRequest: PropTypes.object.isRequired, + providerRequest: PropTypes.exact({ + hostname: PropTypes.string.isRequired, + siteImage: PropTypes.string, + siteTitle: PropTypes.string, + origin: PropTypes.string.isRequired, + extensionId: PropTypes.string, + }).isRequired, }; static contextTypes = { @@ -20,7 +26,6 @@ export default class ProviderApproval extends Component { approveProviderRequestByOrigin={approveProviderRequestByOrigin} rejectProviderRequestByOrigin={rejectProviderRequestByOrigin} origin={providerRequest.origin} - tabID={providerRequest.tabID} siteImage={providerRequest.siteImage} siteTitle={providerRequest.siteTitle} hostname={providerRequest.hostname} From 2673eef3c4f88876d095d0e20e0e21bc312a2af7 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 5 Nov 2019 11:43:48 -0330 Subject: [PATCH 15/28] Redesign approve screen (#7271) * Redesign approve screen * Add translations to approve screen components * Show account in header of approve screen * Use state prop bool for unlimited vs custom check in edit-approval-permission * Set option to custom on input change in edit-approval-permission * Allow setting of approval amount to unlimited in edit-approval-permission * Fix height of confirm-approval popup * Ensure decimals prop passted to confirm-approve.component is correct type * Ensure first param passed to calcTokenValue in confirm-approve.util is the correct type * Fix e2e test of permission editing * Remove unused code from edit-approval-permission.container --- app/_locales/en/messages.json | 50 +++ app/images/user-check.svg | 3 + test/e2e/metamask-ui.spec.js | 71 ++-- .../confirm-page-container-content/index.scss | 1 + ...confirm-page-container-header.component.js | 53 ++- .../confirm-page-container-header/index.scss | 13 + .../confirm-page-container.component.js | 27 +- .../app/confirm-page-container/index.scss | 6 + .../components/app/modal/modal.component.js | 9 +- .../edit-approval-permission.component.js | 170 ++++++++++ .../edit-approval-permission.container.js | 18 ++ .../modals/edit-approval-permission/index.js | 1 + .../edit-approval-permission/index.scss | 167 ++++++++++ ui/app/components/app/modals/index.scss | 2 + ui/app/components/app/modals/modal.js | 26 ++ ui/app/css/itcss/tools/utilities.scss | 4 +- .../with-token-tracker.component.js | 9 +- ui/app/helpers/utils/token-util.js | 5 + .../confirm-approve-content.component.js | 223 +++++++++++++ .../confirm-approve-content/index.js | 1 + .../confirm-approve-content/index.scss | 306 ++++++++++++++++++ .../confirm-approve.component.js | 99 +++++- .../confirm-approve.container.js | 97 +++++- .../confirm-approve/confirm-approve.util.js | 28 ++ ui/app/pages/confirm-approve/index.scss | 1 + .../confirm-transaction-base.component.js | 6 + .../confirm-transaction-base.container.js | 15 +- ui/app/pages/index.scss | 2 + 28 files changed, 1340 insertions(+), 73 deletions(-) create mode 100644 app/images/user-check.svg create mode 100644 ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/index.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/index.scss create mode 100644 ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js create mode 100644 ui/app/pages/confirm-approve/confirm-approve-content/index.js create mode 100644 ui/app/pages/confirm-approve/confirm-approve-content/index.scss create mode 100644 ui/app/pages/confirm-approve/confirm-approve.util.js create mode 100644 ui/app/pages/confirm-approve/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c6b0063e4..53d534a72 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -56,6 +56,10 @@ "acceleratingATransaction": { "message": "* Accelerating a transaction by using a higher gas price increases its chances of getting processed by the network faster, but it is not always guaranteed." }, + "accessAndSpendNotice": { + "message": "$1 may access and spend up to this max amount", + "description": "$1 is the url of the site requesting ability to spend" + }, "accessingYourCamera": { "message": "Accessing your camera..." }, @@ -113,9 +117,20 @@ "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, + "allowOriginSpendToken": { + "message": "Allow $1 to spend your $2?", + "description": "$1 is the url of the site and $2 is the symbol of the token they are requesting to spend" + }, + "allowWithdrawAndSpend": { + "message": "Allow $1 to withdraw and spend up to the following amount:", + "description": "The url of the site that requested permission to 'withdraw and spend'" + }, "amount": { "message": "Amount" }, + "amountWithColon": { + "message": "Amount:" + }, "appDescription": { "message": "An Ethereum Wallet in your Browser", "description": "The description of the application" @@ -384,6 +399,9 @@ "customRPC": { "message": "Custom RPC" }, + "customSpendLimit": { + "message": "Custom Spend Limit" + }, "dataBackupFoundInfo": { "message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts and tokens. Would you like to restore this data now?" }, @@ -444,6 +462,9 @@ "editContact": { "message": "Edit Contact" }, + "editPermission": { + "message": "Edit Permission" + }, "emailUs": { "message": "Email us!" }, @@ -486,6 +507,9 @@ "enterAnAlias": { "message": "Enter an alias" }, + "enterMaxSpendLimit": { + "message": "Enter Max Spend Limit" + }, "enterPassword": { "message": "Enter password" }, @@ -516,6 +540,9 @@ "faster": { "message": "Faster" }, + "feeAssociatedRequest": { + "message": "A fee is associated with this request." + }, "fiat": { "message": "Fiat", "description": "Exchange type" @@ -533,6 +560,9 @@ "fromShapeShift": { "message": "From ShapeShift" }, + "functionApprove": { + "message": "Function: Approve" + }, "functionType": { "message": "Function Type" }, @@ -953,6 +983,9 @@ "privateNetwork": { "message": "Private Network" }, + "proposedApprovalLimit": { + "message": "Proposed Approval Limit" + }, "qrCode": { "message": "Show QR Code" }, @@ -1212,6 +1245,13 @@ "speedUpTransaction": { "message": "Speed up this transaction" }, + "spendLimitPermission": { + "message": "Spend limit permission" + }, + "spendLimitRequestedBy": { + "message": "Spend limit requested by $1", + "description": "Origin of the site requesting the spend limit" + }, "switchNetworks": { "message": "Switch Networks" }, @@ -1308,6 +1348,9 @@ "to": { "message": "To" }, + "toWithColon": { + "message": "To:" + }, "toETHviaShapeShift": { "message": "$1 to ETH via ShapeShift", "description": "system will fill in deposit type in start of message" @@ -1382,6 +1425,10 @@ "message": "We had trouble loading your token balances. You can view them ", "description": "Followed by a link (here) to view token balances" }, + "trustSiteApprovePermission": { + "message": "Do you trust this site? By granting this permission, you’re allowing $1 to withdraw your $2 and automate transactions for you.", + "description": "$1 is the url requesting permission and $2 is the symbol of the currency that the request is for" + }, "tryAgain": { "message": "Try again" }, @@ -1409,6 +1456,9 @@ "unknownCameraError": { "message": "There was an error while trying to access your camera. Please try again..." }, + "unlimited": { + "message": "Unlimited" + }, "unlock": { "message": "Unlock" }, diff --git a/app/images/user-check.svg b/app/images/user-check.svg new file mode 100644 index 000000000..8ba739338 --- /dev/null +++ b/app/images/user-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 868028cd1..143e274cd 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1124,8 +1124,8 @@ describe('MetaMask', function () { await driver.switchTo().window(dapp) await delay(tinyDelayMs) - const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) - await transferTokens.click() + const approveTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) + await approveTokens.click() await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1143,31 +1143,22 @@ describe('MetaMask', function () { }) it('displays the token approval data', async () => { - const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) - dataTab.click() + const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button')) + await fullTxDataButton.click() await delay(regularDelayMs) - const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) + const functionType = await findElement(driver, By.css('.confirm-approve-content__data .confirm-approve-content__small-text')) const functionTypeText = await functionType.getText() - assert.equal(functionTypeText, 'Approve') + assert.equal(functionTypeText, 'Function: Approve') - const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataDiv = await findElement(driver, By.css('.confirm-approve-content__data__data-block')) const confirmDataText = await confirmDataDiv.getText() assert(confirmDataText.match(/0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef4/)) - - const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) - detailsTab.click() - await delay(regularDelayMs) - - const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning')) - const approvalWarningText = await approvalWarning.getText() - assert(approvalWarningText.match(/By approving this/)) - await delay(regularDelayMs) }) it('opens the gas edit modal', async () => { - const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit'))) - await configureGas.click() + const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer')) + await editButtons[0].click() await delay(regularDelayMs) gasModal = await driver.findElement(By.css('span .modal')) @@ -1198,14 +1189,34 @@ describe('MetaMask', function () { await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary')) - assert.equal(await gasFeeInputs[0].getText(), '0.0006') + const gasFeeInEth = await findElement(driver, By.css('.confirm-approve-content__transaction-details-content__secondary-fee')) + assert.equal(await gasFeeInEth.getText(), '0.0006') }) - it('shows the correct recipient', async function () { - const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name')) - const recipientDiv = senderToRecipientDivs[1] - assert.equal(await recipientDiv.getText(), '0x9bc5...fEF4') + it('edits the permission', async () => { + const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer')) + await editButtons[1].click() + await delay(regularDelayMs) + + const permissionModal = await driver.findElement(By.css('span .modal')) + + const radioButtons = await findElements(driver, By.css('.edit-approval-permission__edit-section__radio-button')) + await radioButtons[1].click() + + const customInput = await findElement(driver, By.css('input')) + await delay(50) + await customInput.sendKeys('5') + await delay(regularDelayMs) + + const saveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) + await saveButton.click() + await delay(regularDelayMs) + + await driver.wait(until.stalenessOf(permissionModal)) + + const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text')) + const amountDiv = permissionInfo[0] + assert.equal(await amountDiv.getText(), '5 TST') }) it('submits the transaction', async function () { @@ -1221,7 +1232,7 @@ describe('MetaMask', function () { }, 10000) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/)) + await driver.wait(until.elementTextMatches(txValues[0], /-5\s*TST/)) const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/)) }) @@ -1305,9 +1316,13 @@ describe('MetaMask', function () { }) it('shows the correct recipient', async function () { - const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name')) - const recipientDiv = senderToRecipientDivs[1] - assert.equal(await recipientDiv.getText(), 'Account 2') + const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button')) + await fullTxDataButton.click() + await delay(regularDelayMs) + + const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text')) + const recipientDiv = permissionInfo[1] + assert.equal(await recipientDiv.getText(), '0x2f318C33...C970') }) it('submits the transaction', async function () { diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss index 602a46848..ebc252e73 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss @@ -4,6 +4,7 @@ .confirm-page-container-content { overflow-y: auto; + height: 100%; flex: 1; &__error-container { diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index 4314d21eb..898d59068 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -5,6 +5,8 @@ import { ENVIRONMENT_TYPE_NOTIFICATION, } from '../../../../../../app/scripts/lib/enums' import NetworkDisplay from '../../network-display' +import Identicon from '../../../ui/identicon' +import { addressSlicer } from '../../../../helpers/utils/util' export default class ConfirmPageContainerHeader extends Component { static contextTypes = { @@ -12,13 +14,15 @@ export default class ConfirmPageContainerHeader extends Component { } static propTypes = { + accountAddress: PropTypes.string, + showAccountInHeader: PropTypes.bool, showEdit: PropTypes.bool, onEdit: PropTypes.func, children: PropTypes.node, } renderTop () { - const { onEdit, showEdit } = this.props + const { onEdit, showEdit, accountAddress, showAccountInHeader } = this.props const windowType = window.METAMASK_UI_TYPE const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && windowType !== ENVIRONMENT_TYPE_POPUP @@ -29,22 +33,39 @@ export default class ConfirmPageContainerHeader extends Component { return (
-
- - onEdit()} + { !showAccountInHeader + ?
- { this.context.t('edit') } - -
+ + onEdit()} + > + { this.context.t('edit') } + +
+ : null + } + { showAccountInHeader + ?
+
+ +
+
+ { addressSlicer(accountAddress) } +
+
+ : null + } { !isFullScreen && }
) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss index 44c721446..fb24feb58 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss @@ -9,6 +9,7 @@ border-bottom: 1px solid $geyser; padding: 4px 13px 4px 13px; flex: 0 0 auto; + align-items: center; } &__back-button-container { @@ -28,4 +29,16 @@ font-weight: 400; padding-left: 5px; } + + &__address-container { + display: flex; + align-items: center; + margin-top: 2px; + margin-bottom: 2px; + } + + &__address { + margin-left: 6px; + font-size: 14px; + } } diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index 41d9d5952..86ce2bff7 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -19,6 +19,8 @@ export default class ConfirmPageContainer extends Component { subtitleComponent: PropTypes.node, title: PropTypes.string, titleComponent: PropTypes.node, + hideSenderToRecipient: PropTypes.bool, + showAccountInHeader: PropTypes.bool, // Sender to Recipient fromAddress: PropTypes.string, fromName: PropTypes.string, @@ -104,6 +106,8 @@ export default class ConfirmPageContainer extends Component { lastTx, ofText, requestsWaitingText, + hideSenderToRecipient, + showAccountInHeader, } = this.props const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress) @@ -124,16 +128,21 @@ export default class ConfirmPageContainer extends Component { onEdit()} + showAccountInHeader={showAccountInHeader} + accountAddress={fromAddress} > - + { hideSenderToRecipient + ? null + : + } { contentComponent || ( diff --git a/ui/app/components/app/confirm-page-container/index.scss b/ui/app/components/app/confirm-page-container/index.scss index c0277eff5..3fc72c3a6 100644 --- a/ui/app/components/app/confirm-page-container/index.scss +++ b/ui/app/components/app/confirm-page-container/index.scss @@ -5,3 +5,9 @@ @import 'confirm-detail-row/index'; @import 'confirm-page-container-navigation/index'; + +.page-container { + &__content-component-wrapper { + height: 100%; + } +} diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 44b180ac8..f0fdd3bd5 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -1,10 +1,13 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import Button from '../../ui/button' +import classnames from 'classnames' export default class Modal extends PureComponent { static propTypes = { children: PropTypes.node, + contentClass: PropTypes.string, + containerClass: PropTypes.string, // Header text headerText: PropTypes.string, onClose: PropTypes.func, @@ -36,10 +39,12 @@ export default class Modal extends PureComponent { onCancel, cancelType, cancelText, + contentClass, + containerClass, } = this.props return ( -
+
{ headerText && (
@@ -53,7 +58,7 @@ export default class Modal extends PureComponent {
) } -
+
{ children }
diff --git a/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js new file mode 100644 index 000000000..53ff473e4 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js @@ -0,0 +1,170 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal from '../../modal' +import Identicon from '../../../ui/identicon' +import TextField from '../../../ui/text-field' +import classnames from 'classnames' + +export default class EditApprovalPermission extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + selectedIdentity: PropTypes.object, + tokenAmount: PropTypes.string, + customTokenAmount: PropTypes.string, + tokenSymbol: PropTypes.string, + tokenBalance: PropTypes.string, + setCustomAmount: PropTypes.func, + origin: PropTypes.string, + } + + static contextTypes = { + t: PropTypes.func, + } + + state = { + customSpendLimit: this.props.customTokenAmount, + selectedOptionIsUnlimited: !this.props.customTokenAmount, + } + + renderModalContent () { + const { t } = this.context + const { + hideModal, + selectedIdentity, + tokenAmount, + tokenSymbol, + tokenBalance, + customTokenAmount, + origin, + } = this.props + const { name, address } = selectedIdentity || {} + const { selectedOptionIsUnlimited } = this.state + + return ( +
+
+
+ { t('editPermission') } +
+
hideModal()} + /> +
+
+
+ +
{ name }
+
{ t('balance') }
+
+
+ {`${tokenBalance} ${tokenSymbol}`} +
+
+
+
+ { t('spendLimitPermission') } +
+
+ { t('allowWithdrawAndSpend', [origin]) } +
+
+
this.setState({ selectedOptionIsUnlimited: true })} + > +
+
+ { selectedOptionIsUnlimited &&
} +
+
+
+ { + tokenAmount < tokenBalance + ? t('proposedApprovalLimit') + : t('unlimited') + } +
+
+ { t('spendLimitRequestedBy', [origin]) } +
+
+ {`${tokenAmount} ${tokenSymbol}`} +
+
+
+
+
this.setState({ selectedOptionIsUnlimited: false })} + > +
+
+ { !selectedOptionIsUnlimited &&
} +
+
+
+ { t('customSpendLimit') } +
+
+ { t('enterMaxSpendLimit') } +
+
+ { + this.setState({ customSpendLimit: event.target.value }) + if (selectedOptionIsUnlimited) { + this.setState({ selectedOptionIsUnlimited: false }) + } + }} + fullWidth + margin="dense" + value={ this.state.customSpendLimit } + /> +
+
+
+
+
+ ) + } + + render () { + const { t } = this.context + const { setCustomAmount, hideModal, customTokenAmount } = this.props + const { selectedOptionIsUnlimited, customSpendLimit } = this.state + return ( + { + setCustomAmount(!selectedOptionIsUnlimited ? customSpendLimit : '') + hideModal() + }} + submitText={t('save')} + submitType="primary" + contentClass="edit-approval-permission-modal-content" + containerClass="edit-approval-permission-modal-container" + submitDisabled={ (customSpendLimit === customTokenAmount) && !selectedOptionIsUnlimited } + > + { this.renderModalContent() } + + ) + } +} diff --git a/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js new file mode 100644 index 000000000..ac25fa149 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js @@ -0,0 +1,18 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' +import EditApprovalPermission from './edit-approval-permission.component' +import { getSelectedIdentity } from '../../../../selectors/selectors' + +const mapStateToProps = (state) => { + const modalStateProps = state.appState.modal.modalState.props || {} + return { + selectedIdentity: getSelectedIdentity(state), + ...modalStateProps, + } +} + +export default compose( + withModalProps, + connect(mapStateToProps) +)(EditApprovalPermission) diff --git a/ui/app/components/app/modals/edit-approval-permission/index.js b/ui/app/components/app/modals/edit-approval-permission/index.js new file mode 100644 index 000000000..3f50d3e99 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/index.js @@ -0,0 +1 @@ +export { default } from './edit-approval-permission.container' diff --git a/ui/app/components/app/modals/edit-approval-permission/index.scss b/ui/app/components/app/modals/edit-approval-permission/index.scss new file mode 100644 index 000000000..f400da4c1 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/index.scss @@ -0,0 +1,167 @@ +.edit-approval-permission { + width: 100%; + + &__header, + &__account-info { + display: flex; + justify-content: center; + align-items: center; + position: relative; + border-bottom: 1px solid #d2d8dd; + } + + &__header { + padding: 24px; + + &__close { + position: absolute; + right: 24px; + background-image: url("/images/close-gray.svg"); + width: .75rem; + height: .75rem; + cursor: pointer; + } + } + + &__title { + font-weight: bold; + font-size: 18px; + line-height: 25px; + } + + &__account-info { + justify-content: space-between; + padding: 8px 24px; + + &__account, + &__balance { + font-weight: normal; + font-size: 14px; + color: #24292E; + } + + &__account { + display: flex; + align-items: center; + } + + &__name { + margin-left: 8px; + margin-right: 8px; + } + + &__balance { + color: #6A737D; + } + } + + &__edit-section { + padding: 24px; + + &__title { + font-weight: bold; + font-size: 14px; + line-height: 20px; + color: #24292E; + } + + &__description { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + margin-top: 8px; + } + + &__option { + display: flex; + align-items: flex-start; + margin-top: 20px; + } + + &__radio-button { + width: 18px; + } + + &__option-text { + display: flex; + flex-direction: column; + } + + &__option-label, + &__option-label--selected { + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #474B4D; + } + + &__option-label--selected { + color: #037DD6; + } + + &__option-description { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + margin-top: 8px; + margin-bottom: 6px; + } + + &__option-value { + font-weight: normal; + font-size: 18px; + line-height: 25px; + color: #24292E; + } + + &__radio-button { + position: relative; + width: 18px; + height: 18px; + display: flex; + justify-content: center; + align-items: center; + margin-right: 4px; + } + + &__radio-button-outline, + &__radio-button-outline--selected { + width: 18px; + height: 18px; + background: #DADCDD; + border-radius: 9px; + position: absolute; + } + + &__radio-button-outline--selected { + background: #037DD6; + } + + &__radio-button-fill { + width: 14px; + height: 14px; + background: white; + border-radius: 7px; + position: absolute; + } + + &__radio-button-dot { + width: 8px; + height: 8px; + background: #037DD6; + border-radius: 4px; + position: absolute; + } + } +} + +.edit-approval-permission-modal-content { + padding: 0px; +} + +.edit-approval-permission-modal-container { + max-height: 550px; + width: 100%; +} diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss index d93a41140..da7a27b84 100644 --- a/ui/app/components/app/modals/index.scss +++ b/ui/app/components/app/modals/index.scss @@ -9,3 +9,5 @@ @import 'metametrics-opt-in-modal/index'; @import './add-to-addressbook-modal/index'; + +@import './edit-approval-permission/index'; diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js index c901d6db8..ada758b99 100644 --- a/ui/app/components/app/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -28,6 +28,7 @@ import ClearApprovedOrigins from './clear-approved-origins' import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' import ConfirmDeleteNetwork from './confirm-delete-network' import AddToAddressBookModal from './add-to-addressbook-modal' +import EditApprovalPermission from './edit-approval-permission' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -304,6 +305,31 @@ const MODALS = { }, }, + EDIT_APPROVAL_PERMISSION: { + contents: h(EditApprovalPermission), + mobileModalStyle: { + width: '95vw', + height: '100vh', + top: '50px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + laptopModalStyle: { + width: 'auto', + height: '0px', + top: '80px', + left: '0px', + transform: 'none', + margin: '0 auto', + position: 'relative', + }, + contentStyle: { + borderRadius: '8px', + }, + }, + TRANSACTION_CONFIRMED: { disableBackdropClick: true, contents: h(TransactionConfirmed), diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss index 209614c6b..81eb18d06 100644 --- a/ui/app/css/itcss/tools/utilities.scss +++ b/ui/app/css/itcss/tools/utilities.scss @@ -141,11 +141,11 @@ } .cursor-pointer:hover { - transform: scale(1.1); + transform: scale(1.05); } .cursor-pointer:active { - transform: scale(.95); + transform: scale(.97); } .cursor-disabled { diff --git a/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js index 36f6a6efd..8025dd5bc 100644 --- a/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js +++ b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js @@ -15,6 +15,7 @@ export default function withTokenTracker (WrappedComponent) { this.state = { string: '', symbol: '', + balance: '', error: null, } @@ -78,8 +79,8 @@ export default function withTokenTracker (WrappedComponent) { if (!this.tracker.running) { return } - const [{ string, symbol }] = tokens - this.setState({ string, symbol, error: null }) + const [{ string, symbol, balance }] = tokens + this.setState({ string, symbol, error: null, balance }) } removeListeners () { @@ -91,13 +92,13 @@ export default function withTokenTracker (WrappedComponent) { } render () { - const { string, symbol, error } = this.state - + const { balance, string, symbol, error } = this.state return ( ) diff --git a/ui/app/helpers/utils/token-util.js b/ui/app/helpers/utils/token-util.js index 831d85131..2c4f67fd0 100644 --- a/ui/app/helpers/utils/token-util.js +++ b/ui/app/helpers/utils/token-util.js @@ -128,6 +128,11 @@ export function calcTokenAmount (value, decimals) { return new BigNumber(String(value)).div(multiplier) } +export function calcTokenValue (value, decimals) { + const multiplier = Math.pow(10, Number(decimals || 0)) + return new BigNumber(String(value)).times(multiplier) +} + export function getTokenValue (tokenParams = []) { const valueData = tokenParams.find(param => param.name === '_value') return valueData && valueData.value diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js new file mode 100644 index 000000000..9f11fbb2f --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -0,0 +1,223 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../../components/ui/identicon' +import { + addressSummary, +} from '../../../helpers/utils/util' + +export default class ConfirmApproveContent extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + amount: PropTypes.string, + txFeeTotal: PropTypes.string, + tokenAmount: PropTypes.string, + customTokenAmount: PropTypes.string, + tokenSymbol: PropTypes.string, + siteImage: PropTypes.string, + tokenAddress: PropTypes.string, + showCustomizeGasModal: PropTypes.func, + showEditApprovalPermissionModal: PropTypes.func, + origin: PropTypes.string, + setCustomAmount: PropTypes.func, + tokenBalance: PropTypes.string, + data: PropTypes.string, + toAddress: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionTotal: PropTypes.string, + } + + state = { + showFullTxDetails: false, + } + + renderApproveContentCard ({ + symbol, + title, + showEdit, + onEditClick, + content, + footer, + noBorder, + }) { + return ( +
+
+
{ symbol }
+
{ title }
+ { showEdit &&
onEditClick()} + >Edit
} +
+
+ { content } +
+ { footer } +
+ ) + } + + // TODO: Add "Learn Why" with link to the feeAssociatedRequest text + renderTransactionDetailsContent () { + const { t } = this.context + const { + ethTransactionTotal, + fiatTransactionTotal, + } = this.props + return ( +
+
+ { t('feeAssociatedRequest') } +
+
+
+ { fiatTransactionTotal } +
+
+ { ethTransactionTotal } +
+
+
+ ) + } + + renderPermissionContent () { + const { t } = this.context + const { customTokenAmount, tokenAmount, tokenSymbol, origin, toAddress } = this.props + + return ( +
+
{ t('accessAndSpendNotice', [origin]) }
+
+
{ t('amountWithColon') }
+
{ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }
+
+
+
{ t('toWithColon') }
+
{ addressSummary(toAddress) }
+
+
+ ) + } + + renderDataContent () { + const { t } = this.context + const { data } = this.props + return ( +
+
{ t('functionApprove') }
+
{ data }
+
+ ) + } + + render () { + const { t } = this.context + const { + siteImage, + tokenAmount, + customTokenAmount, + origin, + tokenSymbol, + showCustomizeGasModal, + showEditApprovalPermissionModal, + setCustomAmount, + tokenBalance, + } = this.props + const { showFullTxDetails } = this.state + + return ( +
+
+ +
+
+ { t('allowOriginSpendToken', [origin, tokenSymbol]) } +
+
+ { t('trustSiteApprovePermission', [origin, tokenSymbol]) } +
+
+
showEditApprovalPermissionModal({ customTokenAmount, tokenAmount, tokenSymbol, setCustomAmount, tokenBalance, origin })} + > + { t('editPermission') } +
+
+
+ {this.renderApproveContentCard({ + symbol: , + title: 'Transaction Fee', + showEdit: true, + onEditClick: showCustomizeGasModal, + content: this.renderTransactionDetailsContent(), + noBorder: !showFullTxDetails, + footer:
this.setState({ showFullTxDetails: !this.state.showFullTxDetails })} + > +
+
+ View full transaction details +
+ +
+
, + })} +
+ + { + showFullTxDetails + ? ( +
+
+ {this.renderApproveContentCard({ + symbol: , + title: 'Permission', + content: this.renderPermissionContent(), + showEdit: true, + onEditClick: () => showEditApprovalPermissionModal({ + customTokenAmount, + tokenAmount, + tokenSymbol, + tokenBalance, + setCustomAmount, + }), + })} +
+
+ {this.renderApproveContentCard({ + symbol: , + title: 'Data', + content: this.renderDataContent(), + noBorder: true, + })} +
+
+ ) + : null + } +
+ ) + } +} diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/index.js b/ui/app/pages/confirm-approve/confirm-approve-content/index.js new file mode 100644 index 000000000..8f225387a --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve-content/index.js @@ -0,0 +1 @@ +export { default } from './confirm-approve-content.component' diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/index.scss b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss new file mode 100644 index 000000000..7d3018f6e --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss @@ -0,0 +1,306 @@ +.confirm-approve-content { + display: flex; + flex-flow: column; + align-items: center; + width: 100%; + height: 100%; + + font-family: Roboto; + font-style: normal; + + &__identicon-wrapper { + display: flex; + width: 100%; + justify-content: center; + margin-top: 22px; + padding-left: 24px; + padding-right: 24px; + } + + &__full-tx-content { + display: flex; + flex-flow: column; + align-items: center; + width: 390px; + font-family: Roboto; + font-style: normal; + padding-left: 24px; + padding-right: 24px; + } + + &__card-wrapper { + width: 100%; + } + + &__title { + font-weight: normal; + font-size: 24px; + line-height: 34px; + width: 100%; + display: flex; + justify-content: center; + text-align: center; + margin-top: 22px; + padding-left: 24px; + padding-right: 24px; + } + + &__description { + font-weight: normal; + font-size: 14px; + line-height: 20px; + margin-top: 16px; + margin-bottom: 16px; + color: #6A737D; + text-align: center; + padding-left: 24px; + padding-right: 24px; + } + + &__card, + &__card--no-border { + display: flex; + flex-flow: column; + border-bottom: 1px solid #D2D8DD; + position: relative; + padding-left: 24px; + padding-right: 24px; + + &__bold-text { + font-weight: bold; + font-size: 14px; + line-height: 20px; + } + + &__thin-text { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + } + } + + &__card--no-border { + border-bottom: none; + } + + &__card-header { + display: flex; + flex-flow: row; + margin-top: 20px; + align-items: center; + position: relative; + + &__symbol { + width: auto; + } + + &__symbol--aligned { + width: 100%; + } + + &__title, &__title-value { + font-weight: bold; + font-size: 14px; + line-height: 20px; + } + + &__title { + width: 100%; + margin-left: 16px; + } + + &__title--aligned { + margin-left: 27px; + position: absolute; + width: auto; + } + } + + &__card-content { + margin-top: 6px; + margin-bottom: 12px; + } + + &__card-content--aligned { + margin-left: 42px; + } + + &__transaction-total-symbol { + width: 16px; + display: flex; + justify-content: center; + align-items: center; + height: 16px; + + &__x { + display: flex; + justify-content: center; + align-items: center; + + div { + width: 22px; + height: 2px; + background: #037DD6; + position: absolute; + } + + div:first-of-type { + transform: rotate(45deg); + } + + div:last-of-type { + transform: rotate(-45deg); + } + } + + &__circle { + width: 14px; + height: 14px; + border: 2px solid #037DD6; + border-radius: 50%; + background: white; + position: absolute; + } + } + + &__transaction-details-content { + display: flex; + flex-flow: row; + justify-content: space-between; + + .confirm-approve-content__small-text { + width: 160px; + } + + &__fee { + display: flex; + flex-flow: column; + align-items: flex-end; + text-align: right; + } + + &__primary-fee { + font-weight: bold; + font-size: 18px; + line-height: 25px; + color: #000000; + } + + &__secondary-fee { + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #8C8E94; + } + } + + &__view-full-tx-button-wrapper { + display: flex; + flex-flow: row; + margin-bottom: 16px; + justify-content: center; + + i { + margin-left: 6px; + display: flex; + color: #3099f2; + align-items: center; + } + } + + &__view-full-tx-button { + display: flex; + flex-flow: row; + } + + &__edit-submission-button-container { + display: flex; + flex-flow: row; + padding-top: 15px; + padding-bottom: 30px; + border-bottom: 1px solid #D2D8DD; + width: 100%; + justify-content: center; + padding-left: 24px; + padding-right: 24px; + } + + &__large-text { + font-size: 18px; + line-height: 25px; + color: #24292E; + } + + &__medium-link-text { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #037DD6; + } + + &__medium-text, + &__label { + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #24292E; + } + + &__label { + font-weight: bold; + margin-right: 4px; + } + + &__small-text, &__small-blue-text, &__info-row { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + } + + &__small-blue-text { + color: #037DD6; + } + + &__info-row { + display: flex; + justify-content: space-between; + margin-bottom: 6px; + } + + &__data, + &__permission { + width: 100%; + } + + &__permission { + .flex-row { + margin-top: 14px; + } + } + + &__data { + &__data-block { + overflow-wrap: break-word; + margin-right: 16px; + margin-top: 12px; + } + } + + &__footer { + display: flex; + align-items: flex-end; + margin-top: 16px; + padding-left: 34px; + padding-right: 24px; + + .confirm-approve-content__small-text { + margin-left: 16px; + } + } +} + +.confirm-approve-content--full { + height: auto; +} diff --git a/ui/app/pages/confirm-approve/confirm-approve.component.js b/ui/app/pages/confirm-approve/confirm-approve.component.js index b71eaa1d4..2a40cfa96 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.component.js +++ b/ui/app/pages/confirm-approve/confirm-approve.component.js @@ -1,20 +1,109 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' +import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmApproveContent from './confirm-approve-content' +import { getCustomTxParamsData } from './confirm-approve.util' +import { + calcTokenAmount, +} from '../../helpers/utils/token-util' export default class ConfirmApprove extends Component { + static contextTypes = { + t: PropTypes.func, + } + static propTypes = { + tokenAddress: PropTypes.string, + toAddress: PropTypes.string, tokenAmount: PropTypes.number, tokenSymbol: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionTotal: PropTypes.string, + contractExchangeRate: PropTypes.number, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + showCustomizeGasModal: PropTypes.func, + showEditApprovalPermissionModal: PropTypes.func, + origin: PropTypes.string, + siteImage: PropTypes.string, + tokenTrackerBalance: PropTypes.string, + data: PropTypes.string, + decimals: PropTypes.number, + txData: PropTypes.object, + } + + static defaultProps = { + tokenAmount: 0, + } + + state = { + customPermissionAmount: '', + } + + componentDidUpdate (prevProps) { + const { tokenAmount } = this.props + + if (tokenAmount !== prevProps.tokenAmount) { + this.setState({ customPermissionAmount: tokenAmount }) + } } render () { - const { tokenAmount, tokenSymbol } = this.props + const { + toAddress, + tokenAddress, + tokenSymbol, + tokenAmount, + showCustomizeGasModal, + showEditApprovalPermissionModal, + origin, + siteImage, + tokenTrackerBalance, + data, + decimals, + txData, + ethTransactionTotal, + fiatTransactionTotal, + ...restProps + } = this.props + const { customPermissionAmount } = this.state + + const tokensText = `${tokenAmount} ${tokenSymbol}` + + const tokenBalance = tokenTrackerBalance + ? Number(calcTokenAmount(tokenTrackerBalance, decimals)).toPrecision(9) + : '' return ( - { + this.setState({ customPermissionAmount: newAmount }) + }} + customTokenAmount={String(customPermissionAmount)} + tokenAmount={String(tokenAmount)} + origin={origin} + tokenSymbol={tokenSymbol} + tokenBalance={tokenBalance} + showCustomizeGasModal={() => showCustomizeGasModal(txData)} + showEditApprovalPermissionModal={showEditApprovalPermissionModal} + data={data} + toAddress={toAddress} + ethTransactionTotal={ethTransactionTotal} + fiatTransactionTotal={fiatTransactionTotal} + />} + hideSenderToRecipient={true} + customTxParamsData={customPermissionAmount + ? getCustomTxParamsData(data, { customPermissionAmount, tokenAmount, decimals }) + : null + } + {...restProps} /> ) } diff --git a/ui/app/pages/confirm-approve/confirm-approve.container.js b/ui/app/pages/confirm-approve/confirm-approve.container.js index 5f8bb8f0b..43f5aab90 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.container.js +++ b/ui/app/pages/confirm-approve/confirm-approve.container.js @@ -1,15 +1,102 @@ import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import { + contractExchangeRateSelector, + transactionFeeSelector, +} from '../../selectors/confirm-transaction' +import { showModal } from '../../store/actions' +import { tokenSelector } from '../../selectors/tokens' +import { + getTokenData, +} from '../../helpers/utils/transactions.util' +import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker' +import { + calcTokenAmount, + getTokenToAddress, + getTokenValue, +} from '../../helpers/utils/token-util' import ConfirmApprove from './confirm-approve.component' -import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction' -const mapStateToProps = state => { - const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state - const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) +const mapStateToProps = (state, ownProps) => { + const { match: { params = {} } } = ownProps + const { id: paramsTransactionId } = params + const { + confirmTransaction, + metamask: { currentCurrency, conversionRate, selectedAddressTxList, approvedOrigins, selectedAddress }, + } = state + const { + txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {}, + } = confirmTransaction + + const transaction = selectedAddressTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {} + + const { + ethTransactionTotal, + fiatTransactionTotal, + } = transactionFeeSelector(state, transaction) + const tokens = tokenSelector(state) + const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address) + const { decimals, symbol: tokenSymbol } = currentToken || {} + + const tokenData = getTokenData(data) + const tokenValue = tokenData && getTokenValue(tokenData.params) + const toAddress = tokenData && getTokenToAddress(tokenData.params) + const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toNumber() + const contractExchangeRate = contractExchangeRateSelector(state) + + const { origin } = transaction + const formattedOrigin = origin + ? origin[0].toUpperCase() + origin.slice(1) + : '' + + const { siteImage } = approvedOrigins[origin] || {} return { + toAddress, + tokenAddress, tokenAmount, + currentCurrency, + conversionRate, + contractExchangeRate, + fiatTransactionTotal, + ethTransactionTotal, tokenSymbol, + siteImage, + token: { address: tokenAddress }, + userAddress: selectedAddress, + origin: formattedOrigin, + data, + decimals: Number(decimals), + txData: transaction, } } -export default connect(mapStateToProps)(ConfirmApprove) +const mapDispatchToProps = (dispatch) => { + return { + showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })), + showEditApprovalPermissionModal: ({ + tokenAmount, + customTokenAmount, + tokenSymbol, + tokenBalance, + setCustomAmount, + origin, + }) => dispatch(showModal({ + name: 'EDIT_APPROVAL_PERMISSION', + tokenAmount, + customTokenAmount, + tokenSymbol, + tokenBalance, + setCustomAmount, + origin, + })), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), + withTokenTracker, +)(ConfirmApprove) + diff --git a/ui/app/pages/confirm-approve/confirm-approve.util.js b/ui/app/pages/confirm-approve/confirm-approve.util.js new file mode 100644 index 000000000..be77c65f9 --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve.util.js @@ -0,0 +1,28 @@ +import { decimalToHex } from '../../helpers/utils/conversions.util' +import { calcTokenValue } from '../../helpers/utils/token-util.js' + +export function getCustomTxParamsData (data, { customPermissionAmount, tokenAmount, decimals }) { + if (customPermissionAmount) { + const tokenValue = decimalToHex(calcTokenValue(tokenAmount, decimals)) + + const re = new RegExp('(^.+)' + tokenValue + '$') + const matches = re.exec(data) + + if (!matches || !matches[1]) { + return data + } + let dataWithoutCurrentAmount = matches[1] + const customPermissionValue = decimalToHex(calcTokenValue(Number(customPermissionAmount), decimals)) + + const differenceInLengths = customPermissionValue.length - tokenValue.length + const zeroModifier = dataWithoutCurrentAmount.length - differenceInLengths + if (differenceInLengths > 0) { + dataWithoutCurrentAmount = dataWithoutCurrentAmount.slice(0, zeroModifier) + } else if (differenceInLengths < 0) { + dataWithoutCurrentAmount = dataWithoutCurrentAmount.padEnd(zeroModifier, 0) + } + + const customTxParamsData = dataWithoutCurrentAmount + customPermissionValue + return customTxParamsData + } +} diff --git a/ui/app/pages/confirm-approve/index.scss b/ui/app/pages/confirm-approve/index.scss new file mode 100644 index 000000000..18d7c29e8 --- /dev/null +++ b/ui/app/pages/confirm-approve/index.scss @@ -0,0 +1 @@ +@import 'confirm-approve-content/index'; diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 25725381d..0b46fe9c9 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -105,6 +105,8 @@ export default class ConfirmTransactionBase extends Component { getNextNonce: PropTypes.func, nextNonce: PropTypes.number, tryReverseResolveAddress: PropTypes.func.isRequired, + hideSenderToRecipient: PropTypes.bool, + showAccountInHeader: PropTypes.bool, } state = { @@ -645,6 +647,8 @@ export default class ConfirmTransactionBase extends Component { warning, unapprovedTxCount, transactionCategory, + hideSenderToRecipient, + showAccountInHeader, } = this.props const { submitting, submitError, submitWarning } = this.state @@ -655,6 +659,7 @@ export default class ConfirmTransactionBase extends Component { this.handleCancelAll()} onCancel={() => this.handleCancel()} onSubmit={() => this.handleSubmit()} + hideSenderToRecipient={hideSenderToRecipient} /> ) } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 19602611c..9a238e780 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -46,7 +46,7 @@ const customNonceMerge = txData => customNonceValue ? ({ }) : txData const mapStateToProps = (state, ownProps) => { - const { toAddress: propsToAddress, match: { params = {} } } = ownProps + const { toAddress: propsToAddress, customTxParamsData, match: { params = {} } } = ownProps const { id: paramsTransactionId } = params const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) @@ -133,6 +133,17 @@ const mapStateToProps = (state, ownProps) => { const methodData = getKnownMethodData(state, data) || {} + let fullTxData = { ...txData, ...transaction } + if (customTxParamsData) { + fullTxData = { + ...fullTxData, + txParams: { + ...fullTxData.txParams, + data: customTxParamsData, + }, + } + } + return { balance, fromAddress, @@ -150,7 +161,7 @@ const mapStateToProps = (state, ownProps) => { hexTransactionAmount, hexTransactionFee, hexTransactionTotal, - txData: { ...txData, ...transaction }, + txData: fullTxData, tokenData, methodData, tokenProps, diff --git a/ui/app/pages/index.scss b/ui/app/pages/index.scss index e7242392b..d79b7c28d 100644 --- a/ui/app/pages/index.scss +++ b/ui/app/pages/index.scss @@ -11,3 +11,5 @@ @import 'first-time-flow/index'; @import 'keychains/index'; + +@import 'confirm-approve/index'; From 9ed01dff7adf03bcaea0cf3ed4dcab2e94943135 Mon Sep 17 00:00:00 2001 From: hjlee9182 <46337218+hjlee9182@users.noreply.github.com> Date: Wed, 6 Nov 2019 00:31:28 +0900 Subject: [PATCH 16/28] fix account menu width (#7354) --- ui/app/components/app/account-menu/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss index 435dd6b2a..ee792c783 100644 --- a/ui/app/components/app/account-menu/index.scss +++ b/ui/app/components/app/account-menu/index.scss @@ -2,7 +2,7 @@ position: fixed; z-index: 100; top: 58px; - width: 310px; + width: 320px; @media screen and (max-width: 575px) { right: calc(((100vw - 100%) / 2) + 8px); From b27b568c32ee78d7928838eb0ad4421f6ed54579 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Wed, 6 Nov 2019 12:33:49 -0330 Subject: [PATCH 17/28] Update to gaba@1.8.0 (#7357) --- package.json | 2 +- yarn.lock | 53 ++++++---------------------------------------------- 2 files changed, 7 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 5c155f5ea..2c6d4e875 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "extensionizer": "^1.0.1", "fast-json-patch": "^2.0.4", "fuse.js": "^3.2.0", - "gaba": "^1.7.5", + "gaba": "^1.8.0", "human-standard-token-abi": "^2.0.0", "jazzicon": "^1.2.0", "json-rpc-engine": "^5.1.4", diff --git a/yarn.lock b/yarn.lock index 774d204e7..a5dc454f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9694,19 +9694,6 @@ eth-ens-namehash@^1.0.2: idna-uts46 "^1.0.1" js-sha3 "^0.5.7" -eth-hd-keyring@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-2.0.0.tgz#487ca4f065088ee840739cbad9b853003e79bd06" - integrity sha512-lTeANNPNj/j08sWU7LUQZTsx9NUJaUsiOdVxeP0UI5kke7L+Sd7zJWBmCShudEVG8PkqKLE1KJo08o430sl6rw== - dependencies: - bip39 "^2.2.0" - eth-sig-util "^2.0.1" - ethereumjs-abi "^0.6.5" - ethereumjs-util "^5.1.1" - ethereumjs-wallet "^0.6.0" - events "^1.1.1" - xtend "^4.0.1" - eth-hd-keyring@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-3.4.0.tgz#288e73041f2b3f047b4151fb4b5ab5ad5710b9a6" @@ -9801,22 +9788,6 @@ eth-json-rpc-middleware@^4.1.4, eth-json-rpc-middleware@^4.1.5, eth-json-rpc-mid pify "^3.0.0" safe-event-emitter "^1.0.1" -eth-keyring-controller@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-5.0.1.tgz#695ccde891bb050100c7ad39c4ddf8bfe2da6117" - integrity sha512-u87mdyQ1cOyLyaSME0XzV+qPvM+AO8MMMe+/rpvYxCYRK9KtvGTriyfF67zFcsYVaoRS5Ovc4QiQXCAZEyf3QQ== - dependencies: - bip39 "^2.4.0" - bluebird "^3.5.0" - browser-passworder "^2.0.3" - eth-hd-keyring "^2.0.0" - eth-sig-util "^1.4.0" - eth-simple-keyring "^2.0.0" - ethereumjs-util "^5.1.2" - loglevel "^1.5.0" - obs-store "^4.0.3" - promise-filter "^1.1.0" - eth-keyring-controller@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-5.3.0.tgz#8d85a67b894360ab7d601222ca71df8ed5f456c6" @@ -9901,7 +9872,7 @@ eth-query@^2.0.2, eth-query@^2.1.0, eth-query@^2.1.2: json-rpc-random-id "^1.0.0" xtend "^4.0.1" -eth-sig-util@2.3.0, eth-sig-util@^2.0.1, eth-sig-util@^2.3.0: +eth-sig-util@2.3.0, eth-sig-util@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.3.0.tgz#c54a6ac8e8796f7e25f59cf436982a930e645231" integrity sha512-ugD1AvaggvKaZDgnS19W5qOfepjGc7qHrt7TrAaL54gJw9SHvgIXJ3r2xOMW30RWJZNP+1GlTOy5oye7yXA4xA== @@ -9933,18 +9904,6 @@ eth-sig-util@^2.4.4, eth-sig-util@^2.5.0: tweetnacl "^1.0.0" tweetnacl-util "^0.15.0" -eth-simple-keyring@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-2.0.0.tgz#ef1e97c4aebb7229dce9c0ec5cc84efcd3a76395" - integrity sha512-4dMbkIy2k1qotDTjWINvXG+7tBmofp0YUhlXgcG0+I3w684V46+MAHEkBtD2Y09iEeIB07RDXrezKP9WxOpynA== - dependencies: - eth-sig-util "^2.0.1" - ethereumjs-abi "^0.6.5" - ethereumjs-util "^5.1.1" - ethereumjs-wallet "^0.6.0" - events "^1.1.1" - xtend "^4.0.1" - eth-simple-keyring@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-3.4.0.tgz#01464234b070af42a343a3c451dd58b00ae1a042" @@ -11824,16 +11783,16 @@ fuse.js@^3.4.4: resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.5.tgz#8954fb43f9729bd5dbcb8c08f251db552595a7a6" integrity sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ== -gaba@^1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.7.5.tgz#216cc3196178917a0ae56eda2e1d8981c125364c" - integrity sha512-1S70Sijw5VH4r+pyoZQEoYk+zzsHmwT+xjKPzCo1Ep8A/N1EfcgQxwsD9HNMvPol03Qaf92udIHhry1L7wMjgg== +gaba@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/gaba/-/gaba-1.8.0.tgz#5370e5d662de6aa8e4e41de791da0996a7e12dbe" + integrity sha512-M20fZ6yKRefxgxb82l5Of0VutFxvc1Uxg8LSncaiq5kWQZO1UNe5pkxQc4EQT9rGAcBm6ASv7FG0B04syIELRA== dependencies: await-semaphore "^0.1.3" eth-contract-metadata "^1.9.1" eth-ens-namehash "^2.0.8" eth-json-rpc-infura "^4.0.1" - eth-keyring-controller "^5.0.1" + eth-keyring-controller "^5.3.0" eth-method-registry "1.1.0" eth-phishing-detect "^1.1.13" eth-query "^2.1.2" From 02aebc2e03d045f919d790de9fc0c5d8b7e2f698 Mon Sep 17 00:00:00 2001 From: ricky Date: Wed, 6 Nov 2019 12:04:44 -0500 Subject: [PATCH 18/28] Add onbeforeunload and have it call onCancel (#7335) * Add onbeforeunload and have it call onCancel * Address PR feedback * Get integration tests passing again * Add underscores * Add ENVIRONMENT_TYPE_NOTIFICATION check * Add _beforeUnload + metricsEvent --- .../provider-page-container.component.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js index b89135806..a0a2a7ac4 100644 --- a/ui/app/components/app/provider-page-container/provider-page-container.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container.component.js @@ -2,6 +2,8 @@ import PropTypes from 'prop-types' import React, {PureComponent} from 'react' import { ProviderPageContainerContent, ProviderPageContainerHeader } from '.' import { PageContainerFooter } from '../../ui/page-container' +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' export default class ProviderPageContainer extends PureComponent { static propTypes = { @@ -20,6 +22,9 @@ export default class ProviderPageContainer extends PureComponent { }; componentDidMount () { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.addEventListener('beforeunload', this._beforeUnload) + } this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -29,6 +34,27 @@ export default class ProviderPageContainer extends PureComponent { }) } + _beforeUnload () { + const { origin, rejectProviderRequestByOrigin } = this.props + this.context.metricsEvent({ + eventOpts: { + category: 'Auth', + action: 'Connect', + name: 'Cancel Connect Request Via Notification Close', + }, + }) + this._removeBeforeUnload() + rejectProviderRequestByOrigin(origin) + } + + _removeBeforeUnload () { + window.removeEventListener('beforeunload', this._beforeUnload) + } + + componentWillUnmount () { + this._removeBeforeUnload() + } + onCancel = () => { const { origin, rejectProviderRequestByOrigin } = this.props this.context.metricsEvent({ @@ -38,6 +64,7 @@ export default class ProviderPageContainer extends PureComponent { name: 'Canceled', }, }) + this._removeBeforeUnload() rejectProviderRequestByOrigin(origin) } @@ -50,6 +77,7 @@ export default class ProviderPageContainer extends PureComponent { name: 'Confirmed', }, }) + this._removeBeforeUnload() approveProviderRequestByOrigin(origin) } From ec1f3fa19a2261b04a034ea104e1c04c3acb1439 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sun, 10 Nov 2019 01:30:07 -0500 Subject: [PATCH 19/28] Fix threebox last updated proptype (#7375) * Use child components for multiple notifications component The multiple notifications component has been updated to take its child components as children rather than as a props array, so that the child components are never executed in the case where they aren't needed. * Fix threebox last updated proptype --- .../multiple-notifications.component.js | 13 +++--- ui/app/pages/home/home.component.js | 41 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js index 040890e18..a854a7dda 100644 --- a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js +++ b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types' export default class MultipleNotifications extends PureComponent { static propTypes = { - notifications: PropTypes.array, + children: PropTypes.array, classNames: PropTypes.array, } @@ -14,11 +14,10 @@ export default class MultipleNotifications extends PureComponent { render () { const { showAll } = this.state - const { notifications, classNames = [] } = this.props + const { children, classNames = [] } = this.props - const notificationsToBeRendered = notifications.filter(notificationConfig => notificationConfig.shouldBeRendered) - - if (notificationsToBeRendered.length === 0) { + const childrenToRender = children.filter(child => child) + if (childrenToRender.length === 0) { return null } @@ -29,12 +28,12 @@ export default class MultipleNotifications extends PureComponent { 'home-notification-wrapper--show-first': !showAll, })} > - { notificationsToBeRendered.map(notificationConfig => notificationConfig.component) } + { childrenToRender }
this.setState({ showAll: !showAll })} > - {notificationsToBeRendered.length > 1 ? 1 ? : null}
diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index f96300c9c..531cca851 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -42,7 +42,7 @@ export default class Home extends PureComponent { selectedAddress: PropTypes.string, restoreFromThreeBox: PropTypes.func, setShowRestorePromptToFalse: PropTypes.func, - threeBoxLastUpdated: PropTypes.string, + threeBoxLastUpdated: PropTypes.number, } componentWillMount () { @@ -119,10 +119,10 @@ export default class Home extends PureComponent { + { + showPrivacyModeNotification + ? { @@ -134,11 +134,12 @@ export default class Home extends PureComponent { unsetMigratedPrivacyMode() }} key="home-privacyModeDefault" - />, - }, - { - shouldBeRendered: shouldShowSeedPhraseReminder, - component: + : null + } + { + shouldShowSeedPhraseReminder + ? { @@ -150,12 +151,13 @@ export default class Home extends PureComponent { }} infoText={t('backupApprovalInfo')} key="home-backupApprovalNotice" - />, - }, - { - shouldBeRendered: threeBoxLastUpdated && showRestorePrompt, - component: + : null + } + { + threeBoxLastUpdated && showRestorePrompt + ? , - }, - ]}/> + /> + : null + } + ) : null } From efe240195bc15889f4f27510694128bfe27bed48 Mon Sep 17 00:00:00 2001 From: matteopey Date: Sun, 10 Nov 2019 08:43:36 +0100 Subject: [PATCH 20/28] Hide accounts dropdown scrollbars on Firefox (#7374) --- ui/app/components/app/account-menu/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss index ee792c783..614e19104 100644 --- a/ui/app/components/app/account-menu/index.scss +++ b/ui/app/components/app/account-menu/index.scss @@ -58,6 +58,7 @@ max-height: 256px; position: relative; z-index: 200; + scrollbar-width: none; &::-webkit-scrollbar { display: none; From a8175eb799935d9f7e3a0cb74269e78d7cfc5874 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sun, 10 Nov 2019 21:14:53 -0500 Subject: [PATCH 21/28] Fix advanced tab gas chart (#7380) The gas chart on the advanced tab was not converting the gas price selected into hex before setting it in state, resulting in the UI throwing errors and the price being set incorrectly. It now converts in the same manner as the input fields. --- .../advanced-tab-content.component.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 853889e8d..306dd03a0 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -1,5 +1,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import { + decGWEIToHexWEI, +} from '../../../../../helpers/utils/conversions.util' import Loading from '../../../../ui/loading-screen' import GasPriceChart from '../../gas-price-chart' import AdvancedGasInputs from '../../advanced-gas-inputs' @@ -42,6 +45,11 @@ export default class AdvancedTabContent extends Component { ) } + onGasChartUpdate = (price) => { + const { updateCustomGasPrice } = this.props + updateCustomGasPrice(decGWEIToHexWEI(price)) + } + render () { const { t } = this.context const { @@ -78,7 +86,7 @@ export default class AdvancedTabContent extends Component { ?
{ t('liveGasPricePredictions') }
{!gasEstimatesLoading - ? + ? : }
From 42279c474bbb0b1cb83a3b2521cc8356a376e816 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sun, 10 Nov 2019 21:15:38 -0500 Subject: [PATCH 22/28] Set default advanced tab gas limit (#7379) * Set default advanced tab gas limit The advanced tab of the transaction confirmation screen would enter into an infinite loop and crash if the given gas price was falsy and some interaction was made with the gas limit field. To prevent this infinite loop, a default value of 0 has been set. The user will still need to update the gas limit in order to confirm the transaction, as zero is too low a gas limit (the lowest is 21000). 21000 cannot be the default gas limit at this layer, because the limit used is from a layer above this, which wouldn't have that same 21000 set. * Set default gas limit to minimum allowed A transaction initiated from a dapp might not set a gas limit, which would result in a default of zero being used in the advanced tab. The default gas limit in that case has been changed to 21,000, the minimum allowed gas limit, so that users aren't forced to manually update it. --- .../advanced-gas-inputs/advanced-gas-inputs.component.js | 4 ++-- .../advanced-gas-inputs/advanced-gas-inputs.container.js | 2 +- .../gas-modal-page-container.container.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index 51f3df74a..7fb5aa6f4 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -11,8 +11,8 @@ export default class AdvancedGasInputs extends Component { static propTypes = { updateCustomGasPrice: PropTypes.func, updateCustomGasLimit: PropTypes.func, - customGasPrice: PropTypes.number, - customGasLimit: PropTypes.number, + customGasPrice: PropTypes.number.isRequired, + customGasLimit: PropTypes.number.isRequired, insufficientBalance: PropTypes.bool, customPriceIsSafe: PropTypes.bool, isSpeedUp: PropTypes.bool, diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js index 5dbe89e3d..4fa0d4d94 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js @@ -12,7 +12,7 @@ function convertGasPriceForInputs (gasPriceInHexWEI) { } function convertGasLimitForInputs (gasLimitInHexWEI) { - return parseInt(gasLimitInHexWEI, 16) + return parseInt(gasLimitInHexWEI, 16) || 0 } const mapDispatchToProps = dispatch => { diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 520946a95..c3d214b63 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -84,7 +84,7 @@ const mapStateToProps = (state, ownProps) => { const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, selectedTransaction) const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice - const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit + const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit || '0x5208' const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex) From 66187333b134d1bb818586bb4432bcb86cc93209 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sun, 10 Nov 2019 21:15:50 -0500 Subject: [PATCH 23/28] Prevent attempting ENS resolution on unsupported networks (#7378) The check for whether the network is supported was performed in the constructor, but it was accidentally omitted from the network change handler. --- app/scripts/controllers/ens/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js index 6456f8b53..8451ccb65 100644 --- a/app/scripts/controllers/ens/index.js +++ b/app/scripts/controllers/ens/index.js @@ -26,10 +26,14 @@ class EnsController { this.store = new ObservableStore(initState) networkStore.subscribe((network) => { this.store.putState(initState) - this._ens = new Ens({ - network, - provider, - }) + if (Ens.getNetworkEnsSupport(network)) { + this._ens = new Ens({ + network, + provider, + }) + } else { + delete this._ens + } }) } From 728115171e12cb9953a6b1f8ed16a9f213e44ed1 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Sun, 10 Nov 2019 21:15:59 -0500 Subject: [PATCH 24/28] Catch reverse resolve ENS errors (#7377) The 'reverseResolveAddress' method is intended to return undefined if unable to reverse resolve the given address. Instead it was throwing an error, which surfaced in the UI console. This error is now caught. --- app/scripts/controllers/ens/index.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js index 8451ccb65..81ba5d81e 100644 --- a/app/scripts/controllers/ens/index.js +++ b/app/scripts/controllers/ens/index.js @@ -1,6 +1,7 @@ const ethUtil = require('ethereumjs-util') const ObservableStore = require('obs-store') const punycode = require('punycode') +const log = require('loglevel') const Ens = require('./ens') const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' @@ -51,8 +52,22 @@ class EnsController { return state.ensResolutionsByAddress[address] } - const domain = await this._ens.reverse(address) - const registeredAddress = await this._ens.lookup(domain) + let domain + try { + domain = await this._ens.reverse(address) + } catch (error) { + log.debug(error) + return undefined + } + + let registeredAddress + try { + registeredAddress = await this._ens.lookup(domain) + } catch (error) { + log.debug(error) + return undefined + } + if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) { return undefined } From 78224601b981829345bf85dc40c0bd3f08fec57b Mon Sep 17 00:00:00 2001 From: ricky Date: Sun, 10 Nov 2019 21:17:00 -0500 Subject: [PATCH 25/28] Fix grid-template-columns (#7366) * Fix grid-template-columns * Add fullscreen check --- ui/app/components/app/transaction-list-item/index.scss | 2 +- .../transaction-list-item.component.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/app/components/app/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss index 54a2e9db3..e0c62199e 100644 --- a/ui/app/components/app/transaction-list-item/index.scss +++ b/ui/app/components/app/transaction-list-item/index.scss @@ -13,7 +13,7 @@ width: 100%; padding: 16px 20px; display: grid; - grid-template-columns: 45px 1fr 1fr 1fr; + grid-template-columns: 45px 1fr 1fr 1fr 1fr; grid-template-areas: "identicon action status estimated-time primary-amount" "identicon nonce status estimated-time secondary-amount"; diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index 12350ada6..9ab0105f9 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -12,6 +12,8 @@ import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions' import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' import { getStatusKey } from '../../../helpers/utils/transactions.util' +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' export default class TransactionListItem extends PureComponent { static propTypes = { @@ -196,6 +198,11 @@ export default class TransactionListItem extends PureComponent { ? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to : txParams.to + const isFullScreen = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN + const showEstimatedTime = transactionTimeFeatureActive && + (transaction.id === firstPendingTransactionId) && + isFullScreen + return (
- { transactionTimeFeatureActive && (transaction.id === firstPendingTransactionId) + { showEstimatedTime ? Date: Mon, 11 Nov 2019 11:22:18 -0800 Subject: [PATCH 26/28] Revert "Adds Wyre Widget (#6434)" This reverts commit 6a4df0dc3f620ef0bfe4c1255c1f791390b4280a. --- app/_locales/en/messages.json | 2 +- .../app/modals/deposit-ether-modal.js | 16 +--- ui/app/helpers/constants/routes.js | 2 - ui/app/pages/routes/index.js | 22 ----- ui/vendor/wyre.js | 87 ------------------- 5 files changed, 4 insertions(+), 125 deletions(-) delete mode 100644 ui/vendor/wyre.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 53d534a72..d0e3fd858 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -229,7 +229,7 @@ "message": "Buy ETH with Wyre" }, "buyWithWyreDescription": { - "message": "Wyre lets you use a credit card to deposit ETH right in to your MetaMask account. Open Wyre's widget here to get started." + "message": "Wyre lets you use a credit card to deposit ETH right in to your MetaMask account." }, "buyCoinSwitch": { "message": "Buy on CoinSwitch" diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index 9d8fcffd5..f71e0619e 100644 --- a/ui/app/components/app/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -5,10 +5,6 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../../store/actions') const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util') -const openWyre = require('../../../../vendor/wyre') -const { DEPOSIT_ROUTE } = require('../../../helpers/constants/routes') -const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums') -const { getEnvironmentType } = require('../../../../../app/scripts/lib/util') import Button from '../../ui/button' @@ -125,7 +121,7 @@ DepositEtherModal.prototype.renderRow = function ({ } DepositEtherModal.prototype.render = function () { - const { network, address, toFaucet, toCoinSwitch } = this.props + const { network, toWyre, toCoinSwitch, address, toFaucet } = this.props const isTestNetwork = ['3', '4', '5', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) @@ -186,14 +182,8 @@ DepositEtherModal.prototype.render = function () { title: WYRE_ROW_TITLE, text: WYRE_ROW_TEXT, buttonLabel: this.context.t('continueToWyre'), - onButtonClick: () => { - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser(DEPOSIT_ROUTE) - } else { - openWyre(address) - } - }, - hide: isTestNetwork && !network === '42', + onButtonClick: () => toWyre(address), + hide: isTestNetwork, }), this.renderRow({ diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 44e05ce75..8c2a8b8bc 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -25,7 +25,6 @@ const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' const CONNECT_HARDWARE_ROUTE = '/new-account/connect' const SEND_ROUTE = '/send' -const DEPOSIT_ROUTE = '/deposit' const INITIALIZE_ROUTE = '/initialize' const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome' @@ -95,5 +94,4 @@ module.exports = { CONTACT_MY_ACCOUNTS_EDIT_ROUTE, NETWORKS_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, - DEPOSIT_ROUTE, } diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index 30db8be7c..01e61b1b4 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -8,7 +8,6 @@ import log from 'loglevel' import IdleTimer from 'react-idle-timer' import {getNetworkIdentifier, preferencesSelector} from '../../selectors/selectors' import classnames from 'classnames' -import openWyre from '../../../vendor/wyre' // init import FirstTimeFlow from '../first-time-flow' @@ -68,7 +67,6 @@ import { CONFIRM_TRANSACTION_ROUTE, INITIALIZE_ROUTE, INITIALIZE_UNLOCK_ROUTE, - DEPOSIT_ROUTE, } from '../../helpers/constants/routes' // enums @@ -100,20 +98,6 @@ class Routes extends Component { }) } - componentDidMount () { - const { - location, - modal, - showDepositModal, - selectedAddress, - } = this.props - - if (location.pathname === DEPOSIT_ROUTE && (!modal || !modal.open) && selectedAddress) { - showDepositModal() - openWyre(selectedAddress) - } - } - renderRoutes () { const { autoLogoutTimeLimit, setLastActiveTime } = this.props @@ -132,7 +116,6 @@ class Routes extends Component { - ) @@ -365,9 +348,6 @@ Routes.propTypes = { providerId: PropTypes.string, providerRequests: PropTypes.array, autoLogoutTimeLimit: PropTypes.number, - showDepositModal: PropTypes.func, - modal: PropTypes.object, - selectedAddress: PropTypes.string, } function mapStateToProps (state) { @@ -401,7 +381,6 @@ function mapStateToProps (state) { providerId: getNetworkIdentifier(state), autoLogoutTimeLimit, providerRequests: metamask.providerRequests, - selectedAddress: metamask.selectedAddress, } } @@ -412,7 +391,6 @@ function mapDispatchToProps (dispatch) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), setLastActiveTime: () => dispatch(actions.setLastActiveTime()), - showDepositModal: () => dispatch(actions.showModal({ name: 'DEPOSIT_ETHER' })), } } diff --git a/ui/vendor/wyre.js b/ui/vendor/wyre.js deleted file mode 100644 index 7793b935c..000000000 --- a/ui/vendor/wyre.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -// code taken from https://verify.sendwyre.com/js/verify-module-init-beta.js -'use strict' - -function _classCallCheck (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError('Cannot call a class as a function') - } -} - -function _defineProperties (target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i] - descriptor.enumerable = descriptor.enumerable || false - descriptor.configurable = true - if ('value' in descriptor) descriptor.writable = true - Object.defineProperty(target, descriptor.key, descriptor) - } -} - -function _createClass (Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps) - if (staticProps) _defineProperties(Constructor, staticProps) - return Constructor -} - -function createWyreWidget () { - var ga = null - var Widget = function () { - function a (b) { - var c = !!(1 < arguments.length && void 0 !== arguments[1]) && arguments[1]; - _classCallCheck(this,a), this.debug = c, this.operationHostedWidget="debitcard-hosted", this.operationHostedDialogWidget="debitcard-hosted-dialog", this.operationWidgetLite="debitcard-whitelabel",this.operationDigitalWallet="debitcard",this.operationAch="onramp",this.validOperations=[this.operationWidgetLite,this.operationDigitalWallet,this.operationHostedWidget,this.operationAch,this.operationHostedDialogWidget],this.injectedWidgetOperations=[this.operationWidgetLite,this.operationDigitalWallet],this.iframeWidgetOperations=[this.operationAch],this.queue=[],this.ready=!1,this.eventRegistrations=new Map,null==b.env&&(b.env="production"),this.initParams=b,this.init=this.processClassicInit(b); - var d=b; - console.log("init",b,d),this.getNewWidgetScriptLocation(),this.operationType=d.operation&&d.operation.type||"",this.validateOperationType(),this.injectedWidgetOperations.includes(this.operationType)?(console.log("inject run"),this.attachEvents(),this.createInjectedWidget()):this.iframeWidgetOperations.includes(this.operationType)?(console.log("iframe run"),this.validateInit(),this.attachEvents(),this.createIframe()):this.operationHostedWidget===this.operationType?this.handleHostedWidget():this.operationHostedDialogWidget===this.operationType&&this.emit("ready"); - } - return _createClass( - a, - [ - {key:"validateOperationType",value:function a(){if(!this.validOperations.includes(this.operationType)){var b="supplied operation type >>"+this.operationType+"<< is invalid, valid types are:"+this.validOperations.join(",").toString();throw this.emit("close",{error:b}),this.removeClass(),new Error(b)}}}, - {key:"removeListener",value:function d(a,b){var c=this.eventRegistrations.get(a)||[];c=c.filter(function(a){return a!==b}),this.eventRegistrations.set(a,c);}}, - {key:"removeAllListeners",value:function b(a){a?this.eventRegistrations.set(a,[]):this.eventRegistrations=new Map;}}, - {key:"on",value:function d(a,b){if(!a)throw new Error("must supply an event!");var c=this.eventRegistrations.get(a)||[];c.push(b),this.eventRegistrations.set(a,c);}}, - {key:"open",value:function a(){return this.send("open",{}),console.log(this.operationType),this.operationType==this.operationHostedDialogWidget?void this.handleHostedDialogWidget():void(this.iframe?this.iframe.style.display="block":this.injectedWidget&&(this.injectedWidget.style.display="block"))}}, - {key:"emit",value:function d(a,b){var c=this.eventRegistrations.get(a)||[];c.forEach(function(a){try{a(b||{});}catch(a){console.warn("subscribed widget event handler failure: ",a);}});}}, - {key:"validateInit",value:function a(){switch(this.init.auth.type){case"secretKey":if(this.init.error)return;var b=this.init.auth.secretKey;if(25>b.length)return console.error("Diligently refusing to accept a secret key with length < 25"),this.emit("close",{error:"supplied secretKey is too short"}),void this.removeClass();}}}, - {key:"send",value:function c(a,b){this.queue.push({type:a,payload:b}),this.flush();}}, - {key:"flush",value:function b(){var a=this;this.ready&&(this.queue.forEach(function(b){return a.iframe.contentWindow.postMessage(JSON.stringify(b),"*")}),this.queue=[]);}}, - {key:"attachEvents",value:function d(){var a=this,b=window.addEventListener?"addEventListener":"attachEvent",c="attachEvent"==b?"onmessage":"message";window[b](c,function(b){if("string"==typeof b.data&&0==b.data.indexOf("{")){var c=JSON.parse(b.data);if(console.log("frame",c),!!c.type)switch(c.type){case"ready":a.ready=!0,a.init.web3PresentInParentButNotChild=c.payload&&!c.payload.web3Enabled&&"undefined"!=typeof web3,"function"==typeof ga&&c.payload&&c.payload.gaTrackingCode?(ga("create",c.payload.gaTrackingCode,"auto"),ga(function(b){var c=b.get("clientId");a.send("init",a.init),a.emitReady();})):(a.send("init",a.init),a.emitReady());break;case"close":case"complete":a.close(),a.removeClass(),a.emit(c.type,c.payload);break;case"sign-request":var d=c.payload,e=new Web3(web3.currentProvider);e.personal.sign(e.fromUtf8(d.message),d.address,function(b,c){a.send("sign-response",{signature:c,error:b});});break;case"provider-name":var h=a.getNameOfProvider();a.send("provider-name",h);break;case"accounts":var f=new Web3(web3.currentProvider),g=f.eth.accounts;a.send("accounts-response",{accounts:g});break;default:}}},!1);}}, - {key:"close",value:function a(){this.removeClass(),this.iframe&&document.body.removeChild(this.iframe),this.injectedWidget&&document.body.removeChild(this.injectedWidget),this.injectedWidget=null,this.iframe=null,this.queue=[],this.ready=!1;}}, - {key:"createIframe",value:function b(){var a=Math.round;this.iframe=document.createElement("iframe"),this.iframe.setAttribute("allow","camera;"),this.iframe.style.display="none",this.iframe.style.border="none",this.iframe.style.width="100%",this.iframe.style.height="100%",this.iframe.style.position="fixed",this.iframe.style.zIndex="999999",this.iframe.style.top="0",this.iframe.style.left="0",this.iframe.style.bottom="0",this.iframe.style.right="0",this.iframe.style.backgroundColor="transparent",this.iframe.src=this.getBaseUrl()+"/loader?_cb="+a(new Date().getTime()/1e3),document.body.appendChild(this.iframe);}}, - {key:"getBaseUrl",value:function a(){switch(this.init.env){case"test":return "https://verify.testwyre.com";case"staging":return "https://verify-staging.i.sendwyre.com";case"local":return "http://localhost:8890";case"local_https":return "https://localhost:8890";case"android_emulator":return "http://10.0.2.2:8890";case"production":default:return "https://verify.sendwyre.com";}}}, - {key:"processClassicInit",value:function d(a){if(a.auth)return a;var b=a,c={env:b.env,auth:{type:"metamask"},operation:{type:"onramp",destCurrency:b.destCurrency},apiKey:b.apiKey,web3PresentInParentButNotChild:!1};return b.onExit&&this.on("close",function(a){a.error?(b.onExit(a.error),this.removeClass()):(this.removeClass(),b.onExit(null));}),b.onSuccess&&(this.on("complete",function(){b.onSuccess();}),this.removeClass()),console.debug("converted v1 config to v2, please use this instead: ",c),c}}, - {key:"getNameOfProvider",value:function a(){return web3.currentProvider.isTrust?"trust":"undefined"==typeof __CIPHER__?"undefined"==typeof SOFA?web3.currentProvider.isDDEXWallet?"ddex":"metamask":"coinbase":"cipher"}}, - {key:"getOperationParametersAsQueryString",value:function b(){var a=this;return this.initParams.operation?Object.keys(this.initParams.operation).map(function(b){return b+"="+encodeURIComponent(a.initParams.operation[b])}).join("&"):""}}, - {key:"handleHostedWidget",value:function a(){location.href=this.getPayWidgetLocation()+"/purchase?"+this.getOperationParametersAsQueryString();}}, - {key:"handleHostedDialogWidget",value:function f(){var a=this,b=this.getPayWidgetLocation()+"/purchase?"+this.getOperationParametersAsQueryString(),c=this.openAndCenterDialog(b,360,650),d=window.addEventListener?"addEventListener":"attachEvent",e="attachEvent"===d?"onmessage":"message";window[d](e,function(b){"paymentSuccess"===b.data.type&&a.emit("paymentSuccess",{data:b.data});},!1);}}, - {key:"openAndCenterDialog",value:function o(a,b,c){var d=navigator.userAgent,e=function(){return /\b(iPhone|iP[ao]d)/.test(d)||/\b(iP[ao]d)/.test(d)||/Android/i.test(d)||/Mobile/i.test(d)},f="undefined"==typeof window.screenX?window.screenLeft:window.screenX,g="undefined"==typeof window.screenY?window.screenTop:window.screenY,h="undefined"==typeof window.outerWidth?document.documentElement.clientWidth:window.outerWidth,i="undefined"==typeof window.outerHeight?document.documentElement.clientHeight-22:window.outerHeight,j=e()?null:b,k=e()?null:c,l=0>f?window.screen.width+f:f,m=[];null!==j&&m.push("width="+j),null!==k&&m.push("height="+k),m.push("left="+(l+(h-j)/2)),m.push("top="+(g+(i-k)/2.5)),m.push("scrollbars=1");var n=window.open(a,"Wyre",m.join(","));return window.focus&&n.focus(),n}}, - {key:"getPayWidgetLocation",value:function a(){switch(this.initParams.env){case"test":return "https://pay.testwyre.com";case"staging":return "https://pay-staging.i.sendwyre.com";case"local":return "http://localhost:3000";case"production":default:return "https://pay.sendwyre.com";}}}, - {key:"getNewWidgetScriptLocation",value:function a(){return this.operationType===this.operationWidgetLite?this.getPayWidgetLocation()+"/widget-lite/static/js/widget-lite.js":this.getPayWidgetLocation()+"/digital-wallet-embed.js"}}, - {key:"createInjectedWidget",value:function a(){switch(this.operationType){case"debitcard":{if(this.injectedWidget=document.getElementById("wyre-dropin-widget-container"),!this.injectedWidget){throw this.emit("close",{error:"unable to mount the widget did with id `wyre-dropin-widget-container` not found"}),this.removeClass(),new Error("unable to mount the widget did with id `wyre-dropin-widget-container` not found")}this.injectedWidget.style.display="none",this.handleDebitcardInjection();break}case"debitcard-whitelabel":{if(this.injectedWidget=document.getElementById("wyre-dropin-widget-container"),!this.injectedWidget){throw this.emit("close",{error:"unable to mount the widget did with id `wyre-dropin-widget-container` not found"}),this.removeClass(),new Error("unable to mount the widget did with id `wyre-dropin-widget-container` not found")}this.injectedWidget.style.display="none",this.handleDebitcardWhiteLabelInjection();break}}}}, - {key:"loadJSFile",value:function b(a){return new Promise(function(b,c){var d=document.createElement("script");d.type="text/javascript",d.src=a,d.onload=b,d.onerror=c,d.async=!0,document.getElementsByTagName("head")[0].appendChild(d);})}}, - {key:"handleDebitcardInjection",value:function b(){var a=this;this.loadJSFile(this.getNewWidgetScriptLocation()).then(function(){DigitalWallet.init("wyre-dropin-widget-container",{type:a.operationType,dest:a.initParams.operation.dest,destCurrency:a.initParams.operation.destCurrency,sourceAmount:a.initParams.operation.sourceAmount,paymentMethod:a.initParams.operation.paymentMethod,accountId:a.initParams.accountId},function(b){a.emit("complete",b),a.removeClass();},function(b){a.emit("close",{error:b}),a.removeClass();}),a.send("init",a.init),a.emitReady();}).catch(function(b){throw a.emit("close",{error:b}),a.removeClass(),new Error(b)});}}, - {key:"handleDebitcardWhiteLabelInjection",value:function b(){var a=this;this.loadJSFile(this.getNewWidgetScriptLocation()).then(function(){Wyre2.setData&&Wyre2.widgetLite.appLoaded&&(Wyre2.setData(a.initParams.operation.paymentMethod,a.initParams.operation.sourceAmount,"USD",a.initParams.operation.destCurrency,a.initParams.operation.dest),Wyre2.registerCallback(function(a){this.send("paymentAuthorized",a);},function(a){this.send("error",a),this.removeClass(),console.log(JSON.stringify(a));}),a.send("init",a.init),a.emitReady());}).catch(function(b){throw a.emit("close",{error:b}),a.removeClass(),new Error(b)});}}, - {key:"updateData",value:function e(a,b,c,d){if(this.operationType==this.operationWidgetLite)Wyre2.setData&&Wyre2.widgetLite.appLoaded&&(Wyre2.setData(a,b,"USD",c,d),this.emitReady());else throw this.removeClass(),new Error("this can only be called for operation type "+this.operationWidgetLite)}}, - {key:"emitReady",value:function a(){this.setClassName(),this.emit("ready");}},{key:"removeComponent",value:function a(){}},{key:"setClassName",value:function d(){var a,b,c;a=document.getElementById("wyre-dropin-widget-container"),b="wyre-dropin-widget-container",c=a.className.split(" "),-1==c.indexOf(b)&&(a.className+=" "+b);}},{key:"removeClass",value:function b(){document.getElementById("wyre-dropin-widget-container").style.display="none";var a=document.getElementById("wyre-dropin-widget-container");a.className=a.className.replace(/\bwyre-dropin-widget-container\b/g,"");}} - ] - ),a - }() - var widget=Object.assign(Widget,{Widget:Widget}); - return widget; -} - -function openWyre(address) { - const Wyre = createWyreWidget() - const widget = new Wyre({ - env: 'prod', - operation: { - type: 'debitcard-hosted-dialog', - destCurrency: 'ETH', - dest: `ethereum:${address}`, - }, - }) - - widget.open() -} - -module.exports = openWyre \ No newline at end of file From 2ac96919fc6ce1feca67690b9434528a69a6d8eb Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Mon, 4 Nov 2019 21:52:21 +0000 Subject: [PATCH 27/28] Version v7.5.0 --- CHANGELOG.md | 2 ++ app/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c170c106e..b61b87b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Develop Branch +## 7.5.0 Mon Nov 04 2019 + ## 7.4.0 Tue Oct 29 2019 - [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent` - [#7304](https://github.com/MetaMask/metamask-extension/pull/7304): Move signTypedData signing out to keyrings diff --git a/app/manifest.json b/app/manifest.json index 47b78819f..a882b3818 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "7.4.0", + "version": "7.5.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", From aeabdfdf51ab3060db1a75583cd05131e8cc712f Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 5 Nov 2019 13:55:21 -0400 Subject: [PATCH 28/28] Update changelog for v7.5.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b61b87b28..dae32521b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ ## Current Develop Branch ## 7.5.0 Mon Nov 04 2019 +- [#7328](https://github.com/MetaMask/metamask-extension/pull/7328): ignore known transactions on first broadcast and continue with normal flow +- [#7327](https://github.com/MetaMask/metamask-extension/pull/7327): eth_getTransactionByHash will now check metamask's local history for pending transactions +- [#7333](https://github.com/MetaMask/metamask-extension/pull/7333): Cleanup beforeunload handler after transaction is resolved +- [#7038](https://github.com/MetaMask/metamask-extension/pull/7038): Add support for ZeroNet +- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning +- [#6924](https://github.com/MetaMask/metamask-extension/pull/6924): Add Estimated time to pending tx +- [#7177](https://github.com/MetaMask/metamask-extension/pull/7177): ENS Reverse Resolution support +- [#6891](https://github.com/MetaMask/metamask-extension/pull/6891): New signature request v3 UI +- [#7348](https://github.com/MetaMask/metamask-extension/pull/7348): fix width in first time flow button +- [#7271](https://github.com/MetaMask/metamask-extension/pull/7271): Redesign approve screen +- [#7354](https://github.com/MetaMask/metamask-extension/pull/7354): fix account menu width +- [#7379](https://github.com/MetaMask/metamask-extension/pull/7379): Set default advanced tab gas limit +- [#7380](https://github.com/MetaMask/metamask-extension/pull/7380): Fix advanced tab gas chart +- [#7374](https://github.com/MetaMask/metamask-extension/pull/7374): Hide accounts dropdown scrollbars on Firefox +- [#7357](https://github.com/MetaMask/metamask-extension/pull/7357): Update to gaba@1.8.0 +- [#7335](https://github.com/MetaMask/metamask-extension/pull/7335): Add onbeforeunload and have it call onCancel ## 7.4.0 Tue Oct 29 2019 - [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent`