diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b6d258e..d9e97eec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- [#364](https://github.com/poanetwork/nifty-wallet/pull/364) - Fix notifications order in batch requests + ## 5.0.3 Fri May 01 2020 - [#373](https://github.com/poanetwork/nifty-wallet/pull/373) - (Feature) Add STAKE token diff --git a/app/scripts/background.js b/app/scripts/background.js index c30ac7bce..cd4f71597 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -5,11 +5,11 @@ // this needs to run before anything else require('./lib/setupFetchDebugging')() -const endOfStream = require('end-of-stream') -const pump = require('pump') -const debounce = require('debounce-stream') -const log = require('loglevel') -const extension = require('extensionizer') +import endOfStream from 'end-of-stream' +import pump from 'pump' +import debounce from 'debounce-stream' +import log from 'loglevel' +import extension from 'extensionizer' const LocalStorageStore = require('obs-store/lib/localStorage') const LocalStore = require('./lib/local-store') const storeTransform = require('obs-store/lib/transform') @@ -19,13 +19,11 @@ const Migrator = require('./lib/migrator/') const migrations = require('./migrations/') const PortStream = require('extension-port-stream') const createStreamSink = require('./lib/createStreamSink') -const NotificationManager = require('./lib/notification-manager.js') +import NotificationManager from './lib/notification-manager.js' const MetamaskController = require('./metamask-controller') const rawFirstTimeState = require('./first-time-state') const setupRaven = require('./lib/setupRaven') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') -const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') -const EdgeEncryptor = require('./edge-encryptor') const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code') const getObjStructure = require('./lib/getObjStructure') @@ -51,12 +49,6 @@ global.METAMASK_NOTIFIER = notificationManager const release = platform.getVersion() const raven = setupRaven({ release }) -// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser -// Internet Explorer 6-11 -const isIE = !!document.documentMode -// Edge 20+ -const isEdge = !isIE && !!window.StyleMedia - let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} @@ -70,9 +62,6 @@ let versionedData // initialization flow initialize().catch(log.error) -// setup metamask mesh testing container -setupMetamaskMeshMetrics() - /** * An object representing a transaction, in whatever state it is in. @@ -177,6 +166,7 @@ async function initialize () { async function loadStateFromPersistence () { // migrations const migrator = new Migrator({ migrations }) + migrator.on('error', console.warn) // read from disk // first from preferred, async API: @@ -256,7 +246,7 @@ function setupController (initState, initLangCode) { showUnconfirmedMessage: triggerUi, unlockAccountMessage: triggerUi, showUnapprovedTx: triggerUi, - showWatchAssetUi: showWatchAssetUi, + openPopup: openPopup, // initial state initState, // initial locale code @@ -269,7 +259,6 @@ function setupController (initState, initLangCode) { getOpenMetamaskTabsIds: () => { return openMetamaskTabsIDs }, - encryptor: isEdge ? new EdgeEncryptor() : undefined, }) global.metamaskController = controller @@ -460,28 +449,27 @@ function setupController (initState, initLangCode) { /** * Opens the browser popup for user confirmation */ -function triggerUi () { - extension.tabs.query({ active: true }, tabs => { - const currentlyActiveMetamaskTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id])) - /** - * https://github.com/poanetwork/metamask-extension/issues/19 - * !notificationIsOpen was removed from the check, because notification can be opened, but it can be behind the DApp - * for some reasons. For example, if notification popup was opened, but user moved focus to DApp. - * New transaction, in this case, will not appear in front of DApp. - */ - if (!popupIsOpen && !currentlyActiveMetamaskTab) { - notificationManager.showPopup() - } - }) +async function triggerUi () { + const tabs = await platform.getActiveTabs() + const currentlyActiveMetamaskTab = Boolean(tabs.find((tab) => openMetamaskTabsIDs[tab.id])) + /** + * https://github.com/poanetwork/metamask-extension/issues/19 + * !notificationIsOpen was removed from the check, because notification can be opened, but it can be behind the DApp + * for some reasons. For example, if notification popup was opened, but user moved focus to DApp. + * New transaction, in this case, will not appear in front of DApp. + */ + if (!popupIsOpen && !currentlyActiveMetamaskTab) { + await notificationManager.showPopup() + } } /** * Opens the browser popup for user confirmation of watchAsset * then it waits until user interact with the UI */ -function showWatchAssetUi () { - triggerUi() - return new Promise( +async function openPopup () { + await triggerUi() + await new Promise( (resolve) => { const interval = setInterval(() => { if (!notificationIsOpen) { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 21db813be..501251f2a 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -47,7 +47,7 @@ class PreferencesController { this.diagnostics = opts.diagnostics this.network = opts.network this.store = new ObservableStore(initState) - this.showWatchAssetUi = opts.showWatchAssetUi + this.openPopup = opts.openPopup this._subscribeProviderType() } // PUBLIC METHODS @@ -591,7 +591,7 @@ class PreferencesController { } const tokenOpts = { rawAddress, decimals, symbol, image } this.addSuggestedERC20Asset(tokenOpts) - return this.showWatchAssetUi().then(() => { + return this.openPopup().then(() => { const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress)) return tokenAddresses.length > 0 }) diff --git a/app/scripts/edge-encryptor.js b/app/scripts/edge-encryptor.js deleted file mode 100644 index c5fd0685a..000000000 --- a/app/scripts/edge-encryptor.js +++ /dev/null @@ -1,97 +0,0 @@ -const asmcrypto = require('asmcrypto.js') -const Unibabel = require('browserify-unibabel') - -/** - * A Microsoft Edge-specific encryption class that exposes - * the interface expected by eth-keykeyring-controller - */ -class EdgeEncryptor { - /** - * Encrypts an arbitrary object to ciphertext - * - * @param {string} password Used to generate a key to encrypt the data - * @param {Object} dataObject Data to encrypt - * @returns {Promise} Promise resolving to an object with ciphertext - */ - encrypt (password, dataObject) { - const salt = this._generateSalt() - return this._keyFromPassword(password, salt) - .then(function (key) { - const data = JSON.stringify(dataObject) - const dataBuffer = Unibabel.utf8ToBuffer(data) - const vector = global.crypto.getRandomValues(new Uint8Array(16)) - const resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector) - - const buffer = new Uint8Array(resultbuffer) - const vectorStr = Unibabel.bufferToBase64(vector) - const vaultStr = Unibabel.bufferToBase64(buffer) - return JSON.stringify({ - data: vaultStr, - iv: vectorStr, - salt: salt, - }) - }) - } - - /** - * Decrypts an arbitrary object from ciphertext - * - * @param {string} password Used to generate a key to decrypt the data - * @param {string} text Ciphertext of an encrypted object - * @returns {Promise} Promise resolving to copy of decrypted object - */ - decrypt (password, text) { - const payload = JSON.parse(text) - const salt = payload.salt - return this._keyFromPassword(password, salt) - .then(function (key) { - const encryptedData = Unibabel.base64ToBuffer(payload.data) - const vector = Unibabel.base64ToBuffer(payload.iv) - return new Promise((resolve, reject) => { - let result - try { - result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector) - } catch (err) { - return reject(new Error('Incorrect password')) - } - const decryptedData = new Uint8Array(result) - const decryptedStr = Unibabel.bufferToUtf8(decryptedData) - const decryptedObj = JSON.parse(decryptedStr) - resolve(decryptedObj) - }) - }) - } - - /** - * Retrieves a cryptographic key using a password - * - * @private - * @param {string} password Password used to unlock a cryptographic key - * @param {string} salt Random base64 data - * @returns {Promise} Promise resolving to a derived key - */ - _keyFromPassword (password, salt) { - - const passBuffer = Unibabel.utf8ToBuffer(password) - const saltBuffer = Unibabel.base64ToBuffer(salt) - return new Promise((resolve) => { - const key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000) - resolve(key) - }) - } - - /** - * Generates random base64 encoded data - * - * @private - * @returns {string} Randomized base64 encoded data - */ - _generateSalt (byteCount = 32) { - const view = new Uint8Array(byteCount) - global.crypto.getRandomValues(view) - const b64encoded = btoa(String.fromCharCode.apply(null, view)) - return b64encoded - } -} - -module.exports = EdgeEncryptor diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 6f2dd63ee..4716dc791 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -1,7 +1,7 @@ -const extension = require('extensionizer') -const height = 620 -const width = 360 +import ExtensionPlatform from '../platforms/extension' +const NOTIFICATION_HEIGHT = 620 +const NOTIFICATION_WIDTH = 360 class NotificationManager { @@ -12,47 +12,45 @@ class NotificationManager { * */ + constructor () { + this.platform = new ExtensionPlatform() + } + /** * Either brings an existing MetaMask notification window into focus, or creates a new notification window. New * notification windows are given a 'popup' type. * */ - showPopup () { - this._getPopup((err, popup) => { - if (err) throw err + async showPopup () { + const popup = await this._getPopup() - // Bring focus to chrome popup - if (popup) { - // bring focus to existing chrome popup - extension.windows.update(popup.id, { focused: true }) - } else { - const cb = (currentPopup) => { - this._popupId = currentPopup.id - extension.windows.update(currentPopup.id, { focused: true }) - } - // create new notification popup - const creation = extension.windows.create({ - url: 'notification.html', - type: 'popup', - width, - height, - }, cb) - creation && creation.then && creation.then(cb) - } - }) + // Bring focus to chrome popup + if (popup) { + // bring focus to existing chrome popup + await this.platform.focusWindow(popup.id) + } else { + + // create new notification popup + const popupWindow = await this.platform.openWindow({ + url: 'notification.html', + type: 'popup', + width: NOTIFICATION_WIDTH, + height: NOTIFICATION_HEIGHT, + }) + this._popupId = popupWindow.id + } } /** * Closes a MetaMask notification if it window exists. * */ - closePopup () { - // closes notification popup - this._getPopup((err, popup) => { - if (err) throw err - if (!popup) return - extension.windows.remove(popup.id, console.error) - }) + async closePopup () { + const popup = this._getPopup() + if (!popup) { + return + } + await this.platform.removeWindow(popup.id) } /** @@ -60,39 +58,19 @@ class NotificationManager { * type 'popup') * * @private - * @param {Function} cb A node style callback that to whcih the found notification window will be passed. + * @param {Function} cb - A node style callback that to whcih the found notification window will be passed. * */ - _getPopup (cb) { - this._getWindows((err, windows) => { - if (err) throw err - cb(null, this._getPopupIn(windows)) - }) - } - - /** - * Returns all open MetaMask windows. - * - * @private - * @param {Function} cb A node style callback that to which the windows will be passed. - * - */ - _getWindows (cb) { - // Ignore in test environment - if (!extension.windows) { - return cb() - } - - extension.windows.getAll({}, (windows) => { - cb(null, windows) - }) + async _getPopup () { + const windows = await this.platform.getAllWindows() + return this._getPopupIn(windows) } /** * Given an array of windows, returns the 'popup' that has been opened by MetaMask, or null if no such window exists. * * @private - * @param {array} windows An array of objects containing data about the open MetaMask extension windows. + * @param {array} windows - An array of objects containing data about the open MetaMask extension windows. * */ _getPopupIn (windows) { @@ -104,4 +82,4 @@ class NotificationManager { } -module.exports = NotificationManager +export default NotificationManager diff --git a/app/scripts/lib/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js deleted file mode 100644 index 1d59c0d4a..000000000 --- a/app/scripts/lib/setupMetamaskMeshMetrics.js +++ /dev/null @@ -1,12 +0,0 @@ - -module.exports = setupMetamaskMeshMetrics - -/** - * Injects an iframe into the current document for testing - */ -function setupMetamaskMeshMetrics () { - const testingContainer = document.createElement('iframe') - testingContainer.src = 'https://metamask.github.io/mesh-testing/' - console.log('Injecting Nifty Wallet Mesh testing client') - document.head.appendChild(testingContainer) -} diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 4e3f2a8dd..3286e964b 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -6,6 +6,7 @@ const { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, + ENVIRONMENT_TYPE_BACKGROUND, PLATFORM_FIREFOX, PLATFORM_OPERA, PLATFORM_CHROME, @@ -13,17 +14,6 @@ const { PLATFORM_BRAVE, } = require('./enums') -/** - * Generates an example stack trace - * - * @returns {string} A stack trace - * - */ -function getStack () { - const stack = new Error('Stack trace generator - not an error').stack - return stack -} - /** * Used to determine the window type through which the app is being viewed. * - 'popup' refers to the extension opened through the browser app icon (in top right corner in chrome and firefox) @@ -34,12 +24,15 @@ function getStack () { * */ const getEnvironmentType = (url = window.location.href) => { - if (url.match(/popup.html(?:#.*)*$/)) { + const parsedUrl = new URL(url) + if (parsedUrl.pathname === '/popup.html') { return ENVIRONMENT_TYPE_POPUP - } else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) { + } else if (['/home.html', '/phishing.html'].includes(parsedUrl.pathname)) { return ENVIRONMENT_TYPE_FULLSCREEN - } else { + } else if (parsedUrl.pathname === '/notification.html') { return ENVIRONMENT_TYPE_NOTIFICATION + } else { + return ENVIRONMENT_TYPE_BACKGROUND } } @@ -175,7 +168,6 @@ module.exports = { removeListeners, applyListeners, getPlatform, - getStack, getEnvironmentType, sufficientBalance, hexToBn, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 3421e07de..97a466014 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -111,7 +111,7 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, - showWatchAssetUi: opts.showWatchAssetUi, + openPopup: opts.openPopup, network: this.networkController, }) @@ -1945,7 +1945,7 @@ module.exports = class MetamaskController extends EventEmitter { .sort((block1, block2) => block1.number - block2.number)[recentBlocks.length - 1] const gasPrice = recentBlock && recentBlock.minimumGasPrice && recentBlock.minimumGasPrice.toString() - + if (gasPrice !== '0x' && gasPrice !== '0x0' && gasPrice !== '') { return gasPrice } else { diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index c2ebc3bf3..3d12c7a91 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -12,8 +12,64 @@ class ExtensionPlatform { extension.runtime.reload() } - openWindow ({ url }) { - extension.tabs.create({ url }) + openTab (options) { + return new Promise((resolve, reject) => { + extension.tabs.create(options, (newTab) => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve(newTab) + }) + }) + } + + openWindow (options) { + return new Promise((resolve, reject) => { + extension.windows.create(options, (newWindow) => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve(newWindow) + }) + }) + } + + closeWindow (windowId) { + return new Promise((resolve, reject) => { + extension.windows.remove(windowId, () => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve() + }) + }) + } + + focusWindow (windowId) { + return new Promise((resolve, reject) => { + extension.windows.update(windowId, { focused: true }, () => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve() + }) + }) + } + + getLastFocusedWindow () { + return new Promise((resolve, reject) => { + extension.windows.getLastFocused((windowObject) => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve(windowObject) + }) + }) } closeCurrentWindow () { @@ -22,27 +78,6 @@ class ExtensionPlatform { }) } - /** - * Closes all notifications windows, when action is confirmed in popup - * or closes notification window itself, when action is confirmed from it - */ - closeNotificationWindow () { - return extension.windows.getCurrent((curWindowsDetails) => { - if (curWindowsDetails.type === 'popup') { - return extension.windows.remove(curWindowsDetails.id) - } else { - extension.windows.getAll((windowsDetails) => { - const windowsDetailsFiltered = windowsDetails.filter((windowDetails) => windowDetails.id !== curWindowsDetails.id) - return windowsDetailsFiltered.forEach((windowDetails) => { - if (windowDetails.type === 'popup') { - extension.windows.remove(windowDetails.id) - } - }) - }) - } - }) - } - getVersion () { return extension.runtime.getManifest().version } @@ -57,7 +92,7 @@ class ExtensionPlatform { if (route) { extensionURL += `#${route}` } - this.openWindow({ url: extensionURL }) + this.openTab({ url: extensionURL }) if (getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND) { window.close() } @@ -86,6 +121,30 @@ class ExtensionPlatform { } } + getAllWindows () { + return new Promise((resolve, reject) => { + extension.windows.getAll((windows) => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve(windows) + }) + }) + } + + getActiveTabs () { + return new Promise((resolve, reject) => { + extension.tabs.query({ active: true }, (tabs) => { + const error = checkForError() + if (error) { + return reject(error) + } + return resolve(tabs) + }) + }) + } + currentTab () { return new Promise((resolve, reject) => { extension.tabs.getCurrent((tab) => { diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 5f6c7708e..40657d67c 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -3,11 +3,8 @@ const OldMetaMaskUiCss = require('../../old-ui/css') const startPopup = require('./popup-core') const PortStream = require('extension-port-stream') const { getEnvironmentType } = require('./lib/util') -const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums') import extension from 'extensionizer' const ExtensionPlatform = require('./platforms/extension') -const NotificationManager = require('./lib/notification-manager') -const notificationManager = new NotificationManager() const setupRaven = require('./lib/setupRaven') const log = require('loglevel') @@ -29,7 +26,6 @@ async function start () { // identify window type (popup, notification) const windowType = getEnvironmentType(window.location.href) global.METAMASK_UI_TYPE = windowType - closePopupIfOpen(windowType) // setup stream to background const extensionPort = extension.runtime.connect({ name: windowType }) @@ -51,13 +47,6 @@ async function start () { }) - function closePopupIfOpen (windowType) { - if (windowType !== ENVIRONMENT_TYPE_NOTIFICATION) { - // should close only chrome popup - notificationManager.closePopup() - } - } - function displayCriticalError (err) { container.innerHTML = '
The Nifty Wallet app failed to load: please open and close Nifty Wallet again to restart.
' container.style.height = '80px' diff --git a/old-ui/app/components/connect-hardware/connect-screen.js b/old-ui/app/components/connect-hardware/connect-screen.js index b9700a47d..4d38ae3ea 100644 --- a/old-ui/app/components/connect-hardware/connect-screen.js +++ b/old-ui/app/components/connect-hardware/connect-screen.js @@ -74,7 +74,7 @@ class ConnectScreen extends Component { diff --git a/old-ui/app/components/pending-tx.js b/old-ui/app/components/pending-tx.js index 78516880b..7b0484037 100644 --- a/old-ui/app/components/pending-tx.js +++ b/old-ui/app/components/pending-tx.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types' import clone from 'clone' import log from 'loglevel' -const ethUtil = require('ethereumjs-util') +import ethUtil from 'ethereumjs-util' const BN = ethUtil.BN const hexToBn = require('../../../app/scripts/lib/hex-to-bn') const util = require('../util') @@ -47,12 +47,12 @@ class PendingTx extends Component { isUnlocked: PropTypes.bool, currentCurrency: PropTypes.string, conversionRate: PropTypes.number, - unconfTxListLength: PropTypes.number, provider: PropTypes.object, index: PropTypes.number, blockGasLimit: PropTypes.string, tokensToSend: PropTypes.objectOf(BigNumber), tokensTransferTo: PropTypes.string, + unapprovedTxs: PropTypes.object, } constructor (opts = {}) { @@ -132,12 +132,13 @@ class PendingTx extends Component { const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation } = this.getNavigateTxData() + const balanceBn = hexToBn(balance) const insufficientBalance = balanceBn.lt(maxCost) const dangerousGasLimit = gasBn.gte(saferGasLimitBN) const gasLimitSpecified = txMeta.gasLimitSpecified const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting - const showRejectAll = props.unconfTxListLength > 1 const isNotification = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION @@ -158,7 +159,6 @@ class PendingTx extends Component { } const isError = txMeta.simulationFails || !isValidAddress || insufficientBalance || (dangerousGasLimit && !gasLimitSpecified) - return ( h('div', { @@ -179,7 +179,7 @@ class PendingTx extends Component { h('.flex-row.flex-center', { style: { maxWidth: '100%', - padding: showRejectAll ? '20px 20px 50px 20px' : '20px 20px 20px 20px', + padding: showNavigation ? '20px 20px 50px 20px' : '20px 20px 20px 20px', background: 'linear-gradient(rgb(84, 36, 147), rgb(104, 45, 182))', position: 'relative', }, @@ -197,22 +197,22 @@ class PendingTx extends Component { h('h3', { style: { alignSelf: 'center', - display: props.unconfTxListLength > 1 ? 'block' : 'none', + display: showNavigation ? 'block' : 'none', fontSize: '14px', }, }, [ h('i.fa.white-arrow-left.fa-lg.cursor-pointer', { style: { - display: props.index === 0 ? 'none' : 'inline-block', + display: positionOfCurrentTx === 1 ? 'none' : 'inline-block', }, - onClick: () => props.actions.previousTx(), + onClick: () => props.actions.nextTx(prevTxId), }), - ` ${props.index + 1} of ${props.unconfTxListLength} `, + ` ${positionOfCurrentTx} of ${totalTx} `, h('i.fa.white-arrow-right.fa-lg.cursor-pointer', { style: { - display: props.index + 1 === props.unconfTxListLength ? 'none' : 'inline-block', + display: positionOfCurrentTx === totalTx ? 'none' : 'inline-block', }, - onClick: () => props.actions.nextTx(), + onClick: () => props.actions.nextTx(nextTxId), }), ])], ), @@ -519,7 +519,7 @@ class PendingTx extends Component { onClick: props.cancelTransaction, }, 'Reject'), ]), - showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { + showNavigation ? h('.flex-row.flex-space-around.conf-buttons', { style: { display: 'flex', justifyContent: 'flex-end', @@ -731,6 +731,23 @@ class PendingTx extends Component { } } + getNavigateTxData () { + const { unapprovedTxs, network, txData: { id } = {} } = this.props + const currentNetworkUnapprovedTxs = Object.keys(unapprovedTxs) + .filter((key) => unapprovedTxs[key].metamaskNetworkId === network) + .reduce((acc, key) => ({ ...acc, [key]: unapprovedTxs[key] }), {}) + const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs) + const currentPosition = enumUnapprovedTxs.indexOf(id ? id.toString() : '') + + return { + totalTx: enumUnapprovedTxs.length, + positionOfCurrentTx: currentPosition + 1, + nextTxId: enumUnapprovedTxs[currentPosition + 1], + prevTxId: enumUnapprovedTxs[currentPosition - 1], + showNavigation: enumUnapprovedTxs.length > 1, + } + } + } function forwardCarrat () { @@ -755,7 +772,7 @@ function mapStateToProps (state) { unapprovedMsgs: state.metamask.unapprovedMsgs, unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, unapprovedTypedMessages: state.metamask.unapprovedTypedMessages, - index: state.appState.currentView.pendingTxIndex || 0, + index: state.appState.currentView.key || 0, warning: state.appState.warning, network: state.metamask.network, provider: state.metamask.provider, @@ -764,14 +781,14 @@ function mapStateToProps (state) { currentCurrency: state.metamask.currentCurrency, blockGasLimit: state.metamask.currentBlockGasLimit, computedBalances: state.metamask.computedBalances, + pendingTxIndex: state.appState.currentView.pendingTxIndex || 0, } } const mapDispatchToProps = (dispatch) => { return { actions: { - previousTx: () => dispatch(actions.previousTx()), - nextTx: () => dispatch(actions.nextTx()), + nextTx: (txId) => dispatch(actions.nextTx(txId)), displayWarning: (msg) => dispatch(actions.displayWarning(msg)), goHome: () => dispatch(actions.goHome()), }, diff --git a/old-ui/app/components/shift-list-item.js b/old-ui/app/components/shift-list-item.js index 90ea27c88..d1fd8a31f 100644 --- a/old-ui/app/components/shift-list-item.js +++ b/old-ui/app/components/shift-list-item.js @@ -212,7 +212,7 @@ ShiftListItem.prototype.renderInfo = function () { paddingLeft: '29px', textAlign: 'left', }, - onClick: () => global.platform.openWindow({ url }), + onClick: () => global.platform.openTab({ url }), }, [ h('div', { style: { diff --git a/old-ui/app/conf-tx.js b/old-ui/app/conf-tx.js index e833b4674..8c5bb27eb 100644 --- a/old-ui/app/conf-tx.js +++ b/old-ui/app/conf-tx.js @@ -1,11 +1,11 @@ -const inherits = require('util').inherits -const Component = require('react').Component +import PropTypes from 'prop-types' +import { Component } from 'react' +import { connect } from 'react-redux' const h = require('react-hyperscript') -const connect = require('react-redux').connect const actions = require('../../ui/app/actions') const LoadingIndicator = require('./components/loading') const txHelper = require('../lib/tx-helper') -const log = require('loglevel') +import log from 'loglevel' const { getCurrentKeyring, ifContractAcc } = require('./util') const PendingTx = require('./components/pending-tx') @@ -15,118 +15,224 @@ import PendingTypedMsg from './components/pending-typed-msg' const Loading = require('./components/loading') const { DAI_CODE, POA_SOKOL_CODE, RSK_TESTNET_CODE, GOERLI_TESTNET_CODE } = require('../../app/scripts/controllers/network/enums') const { getMetaMaskAccounts } = require('../../ui/app/selectors') +import BigNumber from 'bignumber.js' -module.exports = connect(mapStateToProps)(ConfirmTxScreen) -function mapStateToProps (state) { - const { metamask, appState } = state - const { screenParams, pendingTxIndex } = appState.currentView - return { - identities: metamask.identities, - accounts: getMetaMaskAccounts(state), - keyrings: metamask.keyrings, - selectedAddress: metamask.selectedAddress, - unapprovedTxs: metamask.unapprovedTxs, - unapprovedMsgs: metamask.unapprovedMsgs, - unapprovedPersonalMsgs: metamask.unapprovedPersonalMsgs, - unapprovedTypedMessages: metamask.unapprovedTypedMessages, - index: pendingTxIndex || 0, - warning: appState.warning, - network: metamask.network, - provider: metamask.provider, - conversionRate: metamask.conversionRate, - currentCurrency: metamask.currentCurrency, - blockGasLimit: metamask.currentBlockGasLimit, - computedBalances: metamask.computedBalances, - isToken: (screenParams && screenParams.isToken), - tokenSymbol: (screenParams && screenParams.tokenSymbol), - tokensToSend: (screenParams && screenParams.tokensToSend), - tokensTransferTo: (screenParams && screenParams.tokensTransferTo), - isContractExecutionByUser: (screenParams && screenParams.isContractExecutionByUser), - } -} - -inherits(ConfirmTxScreen, Component) -function ConfirmTxScreen () { - Component.call(this) -} - -ConfirmTxScreen.prototype.render = function () { - const props = this.props - const { network, unapprovedTxs, currentCurrency, computedBalances, - unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, blockGasLimit } = props - let { conversionRate } = props - - const isTestnet = parseInt(network) === POA_SOKOL_CODE || parseInt(network) === RSK_TESTNET_CODE || parseInt(network) === GOERLI_TESTNET_CODE - const isDai = parseInt(network) === DAI_CODE - if (isTestnet) { - conversionRate = 0 - } else if (isDai) { - conversionRate = 1 +class ConfirmTxScreen extends Component { + static propTypes = { + network: PropTypes.string, + identities: PropTypes.objectOf(PropTypes.object), + keyrings: PropTypes.array, + actions: PropTypes.objectOf(PropTypes.func), + isToken: PropTypes.bool, + isContractExecutionByUser: PropTypes.bool, + selectedAddress: PropTypes.string, + warning: PropTypes.string, + unapprovedTxs: PropTypes.object, + unapprovedMsgs: PropTypes.object, + unapprovedPersonalMsgs: PropTypes.object, + unapprovedTypedMessages: PropTypes.object, + pendingTxIndex: PropTypes.number, + blockGasLimit: PropTypes.string, + accounts: PropTypes.object, + currentCurrency: PropTypes.string, + computedBalances: PropTypes.object, + conversionRate: PropTypes.number, + tokenSymbol: PropTypes.string, + tokensToSend: PropTypes.objectOf(BigNumber), + tokensTransferTo: PropTypes.string, } - const unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) - const ind = props.index || 0 - const txData = unconfTxList[ind] || {} - const txParams = txData.params || {} + render () { + const props = this.props + const { network, unapprovedTxs, currentCurrency, computedBalances, + unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, blockGasLimit } = props + let { conversionRate } = props - log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) - if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + const isTestnet = parseInt(network) === POA_SOKOL_CODE || parseInt(network) === RSK_TESTNET_CODE || parseInt(network) === GOERLI_TESTNET_CODE + const isDai = parseInt(network) === DAI_CODE + if (isTestnet) { + conversionRate = 0 + } else if (isDai) { + conversionRate = 1 + } - const unconfTxListLength = unconfTxList.length + const unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network) + const ind = props.pendingTxIndex || 0 + const txData = unconfTxList[ind] || {} + const txParams = txData.params || {} - return ( + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) - h('.flex-column.flex-grow', { - style: { - width: '100%', - }, - }, [ + const unconfTxListLength = unconfTxList.length - h(LoadingIndicator, { - isLoading: this.state ? !this.state.bypassLoadingScreen : txData.loadingDefaults, - loadingMessage: 'Estimating transaction cost…', - canBypass: true, - bypass: () => { - this.setState({bypassLoadingScreen: true}) + return ( + + h('.flex-column.flex-grow', { + style: { + width: '100%', }, - }), + }, [ - // subtitle and nav + h(LoadingIndicator, { + isLoading: this.state ? !this.state.bypassLoadingScreen : txData.loadingDefaults, + loadingMessage: 'Estimating transaction cost…', + canBypass: true, + bypass: () => { + this.setState({bypassLoadingScreen: true}) + }, + }), - warningIfExists(props.warning), + // subtitle and nav - currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - unconfTxListLength, - computedBalances, - network, - isToken: props.isToken, - tokenSymbol: props.tokenSymbol, - tokensToSend: props.tokensToSend, - tokensTransferTo: props.tokensTransferTo, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress, props.isContractExecutionByUser), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList), - 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), - }), - ]) - ) + warningIfExists(props.warning), + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + unconfTxListLength, + computedBalances, + network, + isToken: props.isToken, + tokenSymbol: props.tokenSymbol, + tokensToSend: props.tokensToSend, + tokensTransferTo: props.tokensTransferTo, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress, props.isContractExecutionByUser), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList), + 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), + }), + ]) + ) + } + + buyEth (address, isContractExecutionByUser, event) { + event.preventDefault() + this.props.actions.buyEthView(address, isContractExecutionByUser) + } + + sendTransaction (txData, event) { + this.stopPropagation(event) + this.props.actions.updateAndApproveTx(txData) + this._checkIfContractExecutionAndUnlockContract(txData) + } + + cancelTransaction (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.actions.cancelTx(txData) + this._checkIfContractExecutionAndUnlockContract(txData) + } + + cancelAllTransactions (unconfTxList, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.actions.cancelTxs(unconfTxList) + this._checkIfMultipleContractExecutionAndUnlockContract(unconfTxList) + } + + signMessage (msgData, event) { + log.info('conf-tx.js: signing message') + const params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.actions.signMsg(params) + } + + stopPropagation (event) { + if (event.stopPropagation) { + event.stopPropagation() + } + } + + signPersonalMessage (msgData, event) { + log.info('conf-tx.js: signing personal message') + const params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.actions.signPersonalMsg(params) + } + + signTypedMessage (msgData, event) { + log.info('conf-tx.js: signing typed message') + const params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.actions.signTypedMsg(params) + } + + cancelMessage (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.actions.cancelMsg(msgData) + } + + cancelPersonalMessage (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.actions.cancelPersonalMsg(msgData) + } + + cancelTypedMessage (msgData, event) { + log.info('canceling typed message') + this.stopPropagation(event) + this.props.actions.cancelTypedMsg(msgData) + } + + _checkIfMultipleContractExecutionAndUnlockContract (unconfTxList) { + const areTxsToOneContractFromTheList = unconfTxList.slice(0).reduce((res, txData, ind, unconfTxList) => { + if (txData.txParams.data && this.props.isContractExecutionByUser) { + const to = txData && txData.txParams && txData.txParams.to + const targetContractIsInTheList = Object.keys(this.props.accounts).some((acc) => acc === to) + if (targetContractIsInTheList && Object.keys(res).length === 0) { + res = { status: true, to } + } else if (res.status && res.to !== to) { + res = { status: false } + unconfTxList.splice(1) + } + } else { + res = { status: false } + unconfTxList.splice(1) + } + return res + }, {}) + + if (areTxsToOneContractFromTheList.status) { + this._unlockContract(areTxsToOneContractFromTheList.to) + } + } + + _checkIfContractExecutionAndUnlockContract (txData) { + if (txData.txParams.data && this.props.isContractExecutionByUser) { + const to = txData && txData.txParams && txData.txParams.to + const targetContractIsInTheList = Object.keys(this.props.accounts).some((acc) => acc === to) + if (targetContractIsInTheList) { + this._unlockContract(to) + } + } + } + + _unlockContract (to) { + const currentKeyring = getCurrentKeyring(to, this.props.network, this.props.keyrings, this.props.identities) + if (ifContractAcc(currentKeyring)) { + this.props.actions.showAccountDetail(to) + } + } } function currentTxView (opts) { @@ -153,119 +259,6 @@ function currentTxView (opts) { } } -ConfirmTxScreen.prototype.buyEth = function (address, isContractExecutionByUser, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address, isContractExecutionByUser)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) - this._checkIfContractExecutionAndUnlockContract(txData) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) - this._checkIfContractExecutionAndUnlockContract(txData) -} - -ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelAllTx(unconfTxList)) - this._checkIfMultipleContractExecutionAndUnlockContract(unconfTxList) -} - -ConfirmTxScreen.prototype.signMessage = function (msgData, event) { - log.info('conf-tx.js: signing message') - const params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signMsg(params)) -} - -ConfirmTxScreen.prototype.stopPropagation = function (event) { - if (event.stopPropagation) { - event.stopPropagation() - } -} - -ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { - log.info('conf-tx.js: signing personal message') - const params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signPersonalMsg(params)) -} - -ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) { - log.info('conf-tx.js: signing typed message') - const params = msgData.msgParams - params.metamaskId = msgData.id - this.stopPropagation(event) - this.props.dispatch(actions.signTypedMsg(params)) -} - -ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { - log.info('canceling message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { - log.info('canceling personal message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelPersonalMsg(msgData)) -} - -ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) { - log.info('canceling typed message') - this.stopPropagation(event) - this.props.dispatch(actions.cancelTypedMsg(msgData)) -} - -ConfirmTxScreen.prototype._checkIfMultipleContractExecutionAndUnlockContract = function (unconfTxList) { - const areTxsToOneContractFromTheList = unconfTxList.slice(0).reduce((res, txData, ind, unconfTxList) => { - if (txData.txParams.data && this.props.isContractExecutionByUser) { - const to = txData && txData.txParams && txData.txParams.to - const targetContractIsInTheList = Object.keys(this.props.accounts).some((acc) => acc === to) - if (targetContractIsInTheList && Object.keys(res).length === 0) { - res = { status: true, to } - } else if (res.status && res.to !== to) { - res = { status: false } - unconfTxList.splice(1) - } - } else { - res = { status: false } - unconfTxList.splice(1) - } - return res - }, {}) - - if (areTxsToOneContractFromTheList.status) { - this._unlockContract(areTxsToOneContractFromTheList.to) - } -} - -ConfirmTxScreen.prototype._checkIfContractExecutionAndUnlockContract = function (txData) { - if (txData.txParams.data && this.props.isContractExecutionByUser) { - const to = txData && txData.txParams && txData.txParams.to - const targetContractIsInTheList = Object.keys(this.props.accounts).some((acc) => acc === to) - if (targetContractIsInTheList) { - this._unlockContract(to) - } - } -} - -ConfirmTxScreen.prototype._unlockContract = function (to) { - const currentKeyring = getCurrentKeyring(to, this.props.network, this.props.keyrings, this.props.identities) - if (ifContractAcc(currentKeyring)) { - this.props.dispatch(actions.showAccountDetail(to)) - } -} - function warningIfExists (warning) { if (warning && // Do not display user rejections on this screen: @@ -277,3 +270,51 @@ function warningIfExists (warning) { }, warning) } } + +function mapStateToProps (state) { + const { metamask, appState } = state + const { screenParams, pendingTxIndex } = appState.currentView + return { + identities: metamask.identities, + accounts: getMetaMaskAccounts(state), + keyrings: metamask.keyrings, + selectedAddress: metamask.selectedAddress, + unapprovedTxs: metamask.unapprovedTxs, + unapprovedMsgs: metamask.unapprovedMsgs, + unapprovedPersonalMsgs: metamask.unapprovedPersonalMsgs, + unapprovedTypedMessages: metamask.unapprovedTypedMessages, + pendingTxIndex: pendingTxIndex || 0, + warning: appState.warning, + network: metamask.network, + provider: metamask.provider, + conversionRate: metamask.conversionRate, + currentCurrency: metamask.currentCurrency, + blockGasLimit: metamask.currentBlockGasLimit, + computedBalances: metamask.computedBalances, + isToken: (screenParams && screenParams.isToken), + tokenSymbol: (screenParams && screenParams.tokenSymbol), + tokensToSend: (screenParams && screenParams.tokensToSend), + tokensTransferTo: (screenParams && screenParams.tokensTransferTo), + isContractExecutionByUser: (screenParams && screenParams.isContractExecutionByUser), + } +} + +function mapDispatchToProps (dispatch) { + return { + actions: { + buyEthView: (address, isContractExecutionByUser) => dispatch(actions.buyEthView(address, isContractExecutionByUser)), + updateAndApproveTx: (txData) => dispatch(actions.updateAndApproveTx(txData)), + cancelTx: (txData) => dispatch(actions.cancelTx(txData)), + cancelTxs: (unconfTxList) => dispatch(actions.cancelTxs(unconfTxList)), + signMsg: (params) => dispatch(actions.signMsg(params)), + signPersonalMsg: (params) => dispatch(actions.signPersonalMsg(params)), + signTypedMsg: (params) => dispatch(actions.signTypedMsg(params)), + cancelMsg: (msgData) => dispatch(actions.cancelMsg(msgData)), + cancelPersonalMsg: (msgData) => dispatch(actions.cancelPersonalMsg(msgData)), + cancelTypedMsg: (msgData) => dispatch(actions.cancelTypedMsg(msgData)), + showAccountDetail: (to) => dispatch(actions.showAccountDetail(to)), + }, + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmTxScreen) diff --git a/test/e2e/elements.js b/test/e2e/elements.js index cd27c895a..a3e503788 100644 --- a/test/e2e/elements.js +++ b/test/e2e/elements.js @@ -298,7 +298,7 @@ module.exports = { accountName: By.className('font-medium color-forest'), edit: By.className('edit-text'), iconCopy: By.className('clipboard cursor-pointer white'), - transactionList: By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'), + transactionList: By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div.flex-column > div > div:nth-child(1)'), buttons: { send: By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)'), buy: By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(3)'), @@ -309,7 +309,7 @@ module.exports = { }, network: By.className('network-name'), sent: { - menu: By.className('wallet-view__tab-history'), + menu: By.css('#wallet-view__tab-history'), tokens: By.className('activeForm right'), }, // balance: By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'), diff --git a/test/e2e/test-cases/import-ganache-seed-phrase.spec.js b/test/e2e/test-cases/import-ganache-seed-phrase.spec.js index 42c268059..abb13995e 100644 --- a/test/e2e/test-cases/import-ganache-seed-phrase.spec.js +++ b/test/e2e/test-cases/import-ganache-seed-phrase.spec.js @@ -60,6 +60,8 @@ const importGanacheSeedPhrase = async (f, account2, password) => { }) it('finds the transaction in the transactions list', async () => { + const sentTab = await f.waitUntilShowUp(screens.main.sent.menu) + await sentTab.click() const transactionAmount = await f.waitUntilShowUp(screens.main.transactionList) assert.equal(await transactionAmount.getText(), '10.0') }) diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 7f1ee3cd0..74ff88127 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -443,7 +443,7 @@ describe('preferences controller', function () { req.params.options = { address, symbol, decimals, image } sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true) - preferencesController.showWatchAssetUi = async () => {} + preferencesController.openPopup = async () => {} await preferencesController._handleWatchAssetERC20(req.params.options) const suggested = preferencesController.getSuggestedTokens() @@ -463,7 +463,7 @@ describe('preferences controller', function () { req.params.options = { address, symbol, decimals, image } sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true) - preferencesController.showWatchAssetUi = async () => { + preferencesController.openPopup = async () => { await preferencesController.addToken(address, symbol, decimals, image) } diff --git a/test/unit/app/edge-encryptor-test.js b/test/unit/app/edge-encryptor-test.js deleted file mode 100644 index 1a6255b36..000000000 --- a/test/unit/app/edge-encryptor-test.js +++ /dev/null @@ -1,101 +0,0 @@ -const assert = require('assert') - -const EdgeEncryptor = require('../../../app/scripts/edge-encryptor') - -var password = 'passw0rd1' -var data = 'some random data' - -global.crypto = global.crypto || { - getRandomValues: function (array) { - for (let i = 0; i < array.length; i++) { - array[i] = Math.random() * 100 - } - return array - }, -} - -describe('EdgeEncryptor', function () { - - const edgeEncryptor = new EdgeEncryptor() - describe('encrypt', function () { - - it('should encrypt the data.', function (done) { - edgeEncryptor.encrypt(password, data) - .then(function (encryptedData) { - assert.notEqual(data, encryptedData) - assert.notEqual(encryptedData.length, 0) - done() - }).catch(function (err) { - done(err) - }) - }) - - it('should return proper format.', function (done) { - edgeEncryptor.encrypt(password, data) - .then(function (encryptedData) { - const encryptedObject = JSON.parse(encryptedData) - assert.ok(encryptedObject.data, 'there is no data') - assert.ok(encryptedObject.iv && encryptedObject.iv.length !== 0, 'there is no iv') - assert.ok(encryptedObject.salt && encryptedObject.salt.length !== 0, 'there is no salt') - done() - }).catch(function (err) { - done(err) - }) - }) - - it('should not return the same twice.', function (done) { - - const encryptPromises = [] - encryptPromises.push(edgeEncryptor.encrypt(password, data)) - encryptPromises.push(edgeEncryptor.encrypt(password, data)) - - Promise.all(encryptPromises).then((encryptedData) => { - assert.equal(encryptedData.length, 2) - assert.notEqual(encryptedData[0], encryptedData[1]) - assert.notEqual(encryptedData[0].length, 0) - assert.notEqual(encryptedData[1].length, 0) - done() - }) - }) - }) - - describe('decrypt', function () { - it('should be able to decrypt the encrypted data.', function (done) { - - edgeEncryptor.encrypt(password, data) - .then(function (encryptedData) { - edgeEncryptor.decrypt(password, encryptedData) - .then(function (decryptedData) { - assert.equal(decryptedData, data) - done() - }) - .catch(function (err) { - done(err) - }) - }) - .catch(function (err) { - done(err) - }) - }) - - it('cannot decrypt the encrypted data with wrong password.', function (done) { - - edgeEncryptor.encrypt(password, data) - .then(function (encryptedData) { - edgeEncryptor.decrypt('wrong password', encryptedData) - .then(function (decryptedData) { - assert.fail('could decrypt with wrong password') - done() - }) - .catch(function (err) { - assert.ok(err instanceof Error) - assert.equal(err.message, 'Incorrect password') - done() - }) - }) - .catch(function (err) { - done(err) - }) - }) - }) -}) diff --git a/ui/app/actions.js b/ui/app/actions.js index be1fd3970..3c6ea9846 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -19,6 +19,7 @@ const { POA, CLASSIC } = require('../../app/scripts/controllers/network/enums') const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util') const WebcamUtils = require('../lib/webcam-utils') +import { getEnvironmentType } from '../../app/scripts/lib/util' const actions = { _setBackgroundConnection: _setBackgroundConnection, @@ -30,6 +31,9 @@ const actions = { MODAL_CLOSE: 'UI_MODAL_CLOSE', showModal: showModal, hideModal: hideModal, + + CLOSE_NOTIFICATION_WINDOW: 'CLOSE_NOTIFICATION_WINDOW', + // sidebar state SIDEBAR_OPEN: 'UI_SIDEBAR_OPEN', SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE', @@ -173,7 +177,6 @@ const actions = { COMPLETED_TX: 'COMPLETED_TX', TRANSACTION_ERROR: 'TRANSACTION_ERROR', NEXT_TX: 'NEXT_TX', - PREVIOUS_TX: 'PREV_TX', EDIT_TX: 'EDIT_TX', signMsg: signMsg, cancelMsg: cancelMsg, @@ -186,14 +189,12 @@ const actions = { signTokenTx: signTokenTx, updateTransaction, updateAndApproveTx, - cancelTx: cancelTx, + cancelTx, cancelTxs, completedTx: completedTx, txError: txError, nextTx: nextTx, editTx, - previousTx: previousTx, - cancelAllTx: cancelAllTx, viewPendingTx: viewPendingTx, VIEW_PENDING_TX: 'VIEW_PENDING_TX', updateTransactionParams, @@ -370,6 +371,8 @@ const actions = { getRequestAccountTabIds, setOpenMetamaskTabsIDs, getOpenMetamaskTabsIds, + closeCurrentNotificationWindow, + closeNotificationWindow, } module.exports = actions @@ -918,9 +921,8 @@ function setCurrentCurrency (currencyCode) { function signMsg (msgData) { log.debug('action - signMsg') - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { log.debug(`actions calling background.signMessage`) background.signMessage(msgData, (err, newState) => { @@ -935,10 +937,7 @@ function signMsg (msgData) { } dispatch(actions.completedTx(msgData.metamaskId)) - - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(closeCurrentNotificationWindow()) return resolve(msgData) }) @@ -948,7 +947,7 @@ function signMsg (msgData) { function signPersonalMsg (msgData) { log.debug('action - signPersonalMsg') - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -966,9 +965,7 @@ function signPersonalMsg (msgData) { dispatch(actions.completedTx(msgData.metamaskId)) - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return resolve(msgData) }) @@ -978,7 +975,7 @@ function signPersonalMsg (msgData) { function signTypedMsg (msgData) { log.debug('action - signTypedMsg') - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -996,9 +993,7 @@ function signTypedMsg (msgData) { dispatch(actions.completedTx(msgData.metamaskId)) - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return resolve(msgData) }) @@ -1205,7 +1200,7 @@ function clearSend () { function sendTx (txData) { log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) - return (dispatch, getState) => { + return (dispatch) => { log.debug(`actions calling background.approveTransaction`) background.approveTransaction(txData.id, (err) => { if (err) { @@ -1214,10 +1209,7 @@ function sendTx (txData) { return log.error(err) } dispatch(actions.completedTx(txData.id)) - - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) }) } } @@ -1267,10 +1259,9 @@ function updateTransaction (txData) { function updateAndApproveTx (txData) { log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) - return (dispatch, getState) => { + return (dispatch) => { log.debug(`actions calling background.updateAndApproveTx`) dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { background.updateAndApproveTransaction(txData, err => { dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) @@ -1293,11 +1284,7 @@ function updateAndApproveTx (txData) { dispatch(actions.clearSend()) dispatch(actions.completedTx(txData.id)) dispatch(actions.hideLoadingIndication()) - dispatch(actions.setCurrentAccountTab('history')) - - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return txData }) @@ -1331,7 +1318,7 @@ function txError (err) { } function cancelMsg (msgData) { - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -1346,9 +1333,7 @@ function cancelMsg (msgData) { dispatch(actions.completedTx(msgData.id)) - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return resolve(msgData) }) @@ -1357,7 +1342,7 @@ function cancelMsg (msgData) { } function cancelPersonalMsg (msgData) { - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -1372,9 +1357,7 @@ function cancelPersonalMsg (msgData) { dispatch(actions.completedTx(id)) - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return resolve(msgData) }) @@ -1383,7 +1366,7 @@ function cancelPersonalMsg (msgData) { } function cancelTypedMsg (msgData) { - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { @@ -1398,9 +1381,7 @@ function cancelTypedMsg (msgData) { dispatch(actions.completedTx(id)) - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return resolve(msgData) }) @@ -1409,12 +1390,11 @@ function cancelTypedMsg (msgData) { } function cancelTx (txData) { - return (dispatch, getState) => { + return (dispatch) => { log.debug(`background.cancelTransaction`) dispatch(actions.showLoadingIndication()) - return new Promise((resolve, reject) => { - background.cancelTransaction(txData.id, err => { + background.cancelTransaction(txData.id, (err) => { if (err) { return reject(err) } @@ -1423,15 +1403,12 @@ function cancelTx (txData) { }) }) .then(() => updateMetamaskStateFromBackground()) - .then(newState => dispatch(actions.updateMetamaskState(newState))) + .then((newState) => dispatch(actions.updateMetamaskState(newState))) .then(() => { dispatch(actions.clearSend()) dispatch(actions.completedTx(txData.id)) dispatch(actions.hideLoadingIndication()) - - if (!hasUnconfirmedTransactions(getState())) { - return global.platform.closeNotificationWindow() - } + dispatch(actions.closeCurrentNotificationWindow()) return txData }) @@ -1444,9 +1421,9 @@ function cancelTx (txData) { * @return {function(*): Promise} */ function cancelTxs (txDataList) { - return async (dispatch, getState) => { + return async (dispatch) => { dispatch(actions.showLoadingIndication()) - const txIds = txDataList.map(({id}) => id) + const txIds = txDataList.map(({ id }) => id) const cancellations = txIds.map((id) => new Promise((resolve, reject) => { background.cancelTransaction(id, (err) => { if (err) { @@ -1468,30 +1445,12 @@ function cancelTxs (txDataList) { dispatch(actions.hideLoadingIndication()) - if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { + if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { return global.platform.closeCurrentWindow() } } } -/** - * @deprecated - * @param {Array} txsData - * @return {Function} - */ -function cancelAllTx (txsData) { - return (dispatch) => { - txsData.forEach((txData, i) => { - background.cancelTransaction(txData.id, () => { - dispatch(actions.completedTx(txData.id)) - if (i === txsData.length - 1) { - dispatch(actions.goHome()) - global.platform.closeNotificationWindow() - } - }) - }) - } -} // // initialize screen // @@ -1762,9 +1721,10 @@ function showConfTxPage (screenParams) { } } -function nextTx () { +function nextTx (txId) { return { type: actions.NEXT_TX, + value: txId, } } @@ -1775,12 +1735,6 @@ function viewPendingTx (txId) { } } -function previousTx () { - return { - type: actions.PREVIOUS_TX, - } -} - function editTx (txId) { return { type: actions.EDIT_TX, @@ -2131,6 +2085,23 @@ function hideModal (payload) { } } +function closeCurrentNotificationWindow () { + return (dispatch, getState) => { + if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION && + !hasUnconfirmedTransactions(getState())) { + global.platform.closeCurrentWindow() + + dispatch(actions.closeNotificationWindow()) + } + } +} + +function closeNotificationWindow () { + return { + type: actions.CLOSE_NOTIFICATION_WINDOW, + } +} + function showSidebar ({ transitionName, type }) { return { type: actions.SIDEBAR_OPEN, @@ -2335,7 +2306,7 @@ function showSendContractPage ({methodSelected, methodABI, inputValues}) { function buyEth (opts) { return (dispatch) => { const url = getBuyEthUrl(opts) - global.platform.openWindow({ url }) + global.platform.openTab({ url }) dispatch({ type: actions.BUY_ETH, }) diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index 22f7e5c23..ed3ea34d5 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -68,7 +68,7 @@ TokenMenuDropdown.prototype.render = function () { onClick: (e) => { e.stopPropagation() const url = ethNetProps.explorerLinks.getExplorerAccountLinkFor(this.props.token.address, this.props.network) - global.platform.openWindow({ url }) + global.platform.openTab({ url }) this.props.onClose() }, text: this.context.t('viewOnEtherscan'), diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index b86adde20..230da2438 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -79,7 +79,8 @@ function reduceApp (state, action) { customHdPaths: customHdPaths, }, state.appState) - let curPendingTxIndex = appState.currentView.pendingTxIndex || 0 + const curPendingTxIndex = appState.currentView.pendingTxIndex || 0 + const curPendingTxId = appState.currentView.pendingTxId || 0 switch (action.type) { // dropdown methods @@ -507,6 +508,7 @@ function reduceApp (state, action) { currentView: { name: 'confTx', pendingTxIndex: action.id ? indexForPending(state, action.id) : 0, + pendingTxId: action.id, screenParams: action.value, }, transForward: action.transForward, @@ -559,11 +561,14 @@ function reduceApp (state, action) { } case actions.NEXT_TX: + const increment = (action.value - curPendingTxId) return extend(appState, { transForward: true, currentView: { name: 'confTx', - pendingTxIndex: ++curPendingTxIndex, + pendingTxIndex: curPendingTxIndex + increment, + pendingTxId: action.value, + index: curPendingTxIndex + increment, warning: null, }, }) @@ -575,16 +580,7 @@ function reduceApp (state, action) { currentView: { name: 'confTx', pendingTxIndex, - warning: null, - }, - }) - - case actions.PREVIOUS_TX: - return extend(appState, { - transForward: false, - currentView: { - name: 'confTx', - pendingTxIndex: --curPendingTxIndex, + pendingTxId: action.value, warning: null, }, }) diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index cf2848dcc..594fa93fe 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -6,6 +6,8 @@ import { roundExponential } from '../helpers/confirm-transaction/util' const unapprovedTxsSelector = state => state.metamask.unapprovedTxs const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs +const unapprovedDecryptMsgsSelector = (state) => state.metamask.unapprovedDecryptMsgs +const unapprovedEncryptionPublicKeyMsgsSelector = (state) => state.metamask.unapprovedEncryptionPublicKeyMsgs const unapprovedTypedMessagesSelector = state => state.metamask.unapprovedTypedMessages const networkSelector = state => state.metamask.network @@ -13,18 +15,24 @@ export const unconfirmedTransactionsListSelector = createSelector( unapprovedTxsSelector, unapprovedMsgsSelector, unapprovedPersonalMsgsSelector, + unapprovedDecryptMsgsSelector, + unapprovedEncryptionPublicKeyMsgsSelector, unapprovedTypedMessagesSelector, networkSelector, ( unapprovedTxs = {}, unapprovedMsgs = {}, unapprovedPersonalMsgs = {}, + unapprovedDecryptMsgs = {}, + unapprovedEncryptionPublicKeyMsgs = {}, unapprovedTypedMessages = {}, network, ) => txHelper( unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, + unapprovedDecryptMsgs, + unapprovedEncryptionPublicKeyMsgs, unapprovedTypedMessages, network, ) || [], @@ -34,12 +42,16 @@ export const unconfirmedTransactionsHashSelector = createSelector( unapprovedTxsSelector, unapprovedMsgsSelector, unapprovedPersonalMsgsSelector, + unapprovedDecryptMsgsSelector, + unapprovedEncryptionPublicKeyMsgsSelector, unapprovedTypedMessagesSelector, networkSelector, ( unapprovedTxs = {}, unapprovedMsgs = {}, unapprovedPersonalMsgs = {}, + unapprovedDecryptMsgs = {}, + unapprovedEncryptionPublicKeyMsgs = {}, unapprovedTypedMessages = {}, network, ) => { @@ -58,6 +70,8 @@ export const unconfirmedTransactionsHashSelector = createSelector( ...filteredUnapprovedTxs, ...unapprovedMsgs, ...unapprovedPersonalMsgs, + ...unapprovedDecryptMsgs, + ...unapprovedEncryptionPublicKeyMsgs, ...unapprovedTypedMessages, } }, @@ -65,18 +79,24 @@ export const unconfirmedTransactionsHashSelector = createSelector( const unapprovedMsgCountSelector = state => state.metamask.unapprovedMsgCount const unapprovedPersonalMsgCountSelector = state => state.metamask.unapprovedPersonalMsgCount +const unapprovedDecryptMsgCountSelector = (state) => state.metamask.unapprovedDecryptMsgCount +const unapprovedEncryptionPublicKeyMsgCountSelector = (state) => state.metamask.unapprovedEncryptionPublicKeyMsgCount const unapprovedTypedMessagesCountSelector = state => state.metamask.unapprovedTypedMessagesCount export const unconfirmedTransactionsCountSelector = createSelector( unapprovedTxsSelector, unapprovedMsgCountSelector, unapprovedPersonalMsgCountSelector, + unapprovedDecryptMsgCountSelector, + unapprovedEncryptionPublicKeyMsgCountSelector, unapprovedTypedMessagesCountSelector, networkSelector, ( unapprovedTxs = {}, unapprovedMsgCount = 0, unapprovedPersonalMsgCount = 0, + unapprovedDecryptMsgCount = 0, + unapprovedEncryptionPublicKeyMsgCount = 0, unapprovedTypedMessagesCount = 0, network, ) => { @@ -86,7 +106,7 @@ export const unconfirmedTransactionsCountSelector = createSelector( }) return filteredUnapprovedTxIds.length + unapprovedTypedMessagesCount + unapprovedMsgCount + - unapprovedPersonalMsgCount + unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount }, ) @@ -132,15 +152,16 @@ export const tokenAmountAndToAddressSelector = createSelector( let tokenAmount = 0 if (params && params.length) { - const toParam = params.find(param => param.name === TOKEN_PARAM_TO) - const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE) + const toParam = params.find((param) => param.name === TOKEN_PARAM_TO) + const valueParam = params.find((param) => param.name === TOKEN_PARAM_VALUE) toAddress = toParam ? toParam.value : params[0].value const value = valueParam ? Number(valueParam.value) : Number(params[1].value) - tokenAmount = roundExponential(value) if (tokenDecimals) { - tokenAmount = calcTokenAmount(value, tokenDecimals) + tokenAmount = calcTokenAmount(value, tokenDecimals).toNumber() } + + tokenAmount = roundExponential(tokenAmount) } return { @@ -158,11 +179,11 @@ export const approveTokenAmountAndToAddressSelector = createSelector( let tokenAmount = 0 if (params && params.length) { - toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value - const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value) + toAddress = params.find((param) => param.name === TOKEN_PARAM_SPENDER).value + const value = Number(params.find((param) => param.name === TOKEN_PARAM_VALUE).value) if (tokenDecimals) { - tokenAmount = calcTokenAmount(value, tokenDecimals) + tokenAmount = calcTokenAmount(value, tokenDecimals).toNumber() } tokenAmount = roundExponential(tokenAmount) @@ -183,11 +204,11 @@ export const sendTokenTokenAmountAndToAddressSelector = createSelector( let tokenAmount = 0 if (params && params.length) { - toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value - let value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value) + toAddress = params.find((param) => param.name === TOKEN_PARAM_TO).value + let value = Number(params.find((param) => param.name === TOKEN_PARAM_VALUE).value) if (tokenDecimals) { - value = calcTokenAmount(value, tokenDecimals) + value = calcTokenAmount(value, tokenDecimals).toNumber() } tokenAmount = roundExponential(value) diff --git a/ui/index.js b/ui/index.js index e5fab15a3..2ae86f616 100644 --- a/ui/index.js +++ b/ui/index.js @@ -54,9 +54,8 @@ async function startApp (metamaskState, accountManager, opts) { const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network) const numberOfUnapprivedTx = unapprovedTxsAll.length if (numberOfUnapprivedTx > 0) { - store.dispatch(actions.showConfTxPage({ - id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id, + id: unapprovedTxsAll[0].id, })) }