diff options
Diffstat (limited to 'app/scripts')
32 files changed, 604 insertions, 427 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index cca0d4709..8e65bd5a4 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -116,7 +116,6 @@ setupMetamaskMeshMetrics() * @property {boolean} useBlockie - Indicates preferred user identicon format. True for blockie, false for Jazzicon. * @property {Object} featureFlags - An object for optional feature flags. * @property {string} networkEndpointType - TODO: Document - * @property {boolean} isRevealingSeedWords - True if seed words are currently being recovered, and should be shown to user. * @property {boolean} welcomeScreen - True if welcome screen should be shown. * @property {string} currentLocale - A locale string matching the user's preferred display language. * @property {Object} provider - The current selected network provider. @@ -253,7 +252,6 @@ function setupController (initState, initLangCode) { const controller = new MetamaskController({ // User confirmation callbacks: showUnconfirmedMessage: triggerUi, - unlockAccountMessage: triggerUi, showUnapprovedTx: triggerUi, openPopup: openPopup, closePopup: notificationManager.closePopup.bind(notificationManager), diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 0c55ae39f..7415c5fe9 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -10,7 +10,7 @@ const extension = require('extensionizer') const PortStream = require('extension-port-stream') const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() -const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' +const inpageSuffix = '//# sourceURL=' + extension.runtime.getURL('inpage.js') + '\n' const inpageBundle = inpageContent + inpageSuffix // Eventually this streaming injection could be replaced with: @@ -114,6 +114,7 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { async function setupPublicApi (outStream) { const api = { + forceReloadSite: (cb) => cb(null, forceReloadSite()), getSiteMetadata: (cb) => cb(null, getSiteMetadata()), } const dnode = Dnode(api) @@ -306,3 +307,10 @@ async function domIsReady () { // wait for load await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) } + +/** + * Reloads the site + */ +function forceReloadSite () { + window.location.reload() +} diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index e04ce2ef7..caa061df4 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -65,7 +65,7 @@ class ComputedbalancesController { syncAllAccountsFromStore (store) { const upstream = Object.keys(store.accounts) const balances = Object.keys(this.balances) - .map(address => this.balances[address]) + .map(address => this.balances[address]) // Follow new addresses for (const address in balances) { diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index e6940c613..e6e993073 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -47,14 +47,14 @@ class DetectTokensController { } tokensToDetect.forEach((tokenAddress, index) => { const balance = result[index] - if (!balance.isZero()) { + if (balance && !balance.isZero()) { this._preferences.addToken(tokenAddress, contracts[tokenAddress].symbol, contracts[tokenAddress].decimals) } }) }) } - /** + /** * Find if selectedAddress has tokens with contract in contractAddress. * * @param {string} contractAddress Hex address of the token contract to explore. diff --git a/app/scripts/controllers/network/contract-addresses.js b/app/scripts/controllers/network/contract-addresses.js index 5cd7da1d0..f9385accd 100644 --- a/app/scripts/controllers/network/contract-addresses.js +++ b/app/scripts/controllers/network/contract-addresses.js @@ -4,8 +4,8 @@ const SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN = '0xb8e671734ce5c8d7dfbbea5574fa4cf3 const SINGLE_CALL_BALANCES_ADDRESS_KOVAN = '0xb1d3fbb2f83aecd196f474c16ca5d9cffa0d0ffc' module.exports = { - SINGLE_CALL_BALANCES_ADDRESS, - SINGLE_CALL_BALANCES_ADDRESS_RINKEBY, - SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN, - SINGLE_CALL_BALANCES_ADDRESS_KOVAN, + SINGLE_CALL_BALANCES_ADDRESS, + SINGLE_CALL_BALANCES_ADDRESS_RINKEBY, + SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN, + SINGLE_CALL_BALANCES_ADDRESS_KOVAN, } diff --git a/app/scripts/controllers/onboarding.js b/app/scripts/controllers/onboarding.js new file mode 100644 index 000000000..18fec4993 --- /dev/null +++ b/app/scripts/controllers/onboarding.js @@ -0,0 +1,43 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +/** + * @typedef {Object} InitState + * @property {Boolean} seedPhraseBackedUp Indicates whether the user has completed the seed phrase backup challenge + */ + +/** + * @typedef {Object} OnboardingOptions + * @property {InitState} initState The initial controller state + */ + +/** + * Controller responsible for maintaining + * a cache of account balances in local storage + */ +class OnboardingController { + /** + * Creates a new controller instance + * + * @param {OnboardingOptions} [opts] Controller configuration parameters + */ + constructor (opts = {}) { + const initState = extend({ + seedPhraseBackedUp: null, + }, opts.initState) + this.store = new ObservableStore(initState) + } + + setSeedPhraseBackedUp (newSeedPhraseBackUpState) { + this.store.updateState({ + seedPhraseBackedUp: newSeedPhraseBackUpState, + }) + } + + getSeedPhraseBackedUp () { + return this.store.getState().seedPhraseBackedUp + } + +} + +module.exports = OnboardingController diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index acf952bb1..d480834f5 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -49,13 +49,12 @@ class PreferencesController { currentLocale: opts.initLangCode, identities: {}, lostIdentities: {}, - seedWords: null, forgottenPassword: false, preferences: { useNativeCurrencyAsPrimaryCurrency: true, }, completedOnboarding: false, - completedUiMigration: true, + migratedPrivacyMode: false, metaMetricsId: null, metaMetricsSendCount: 0, }, opts.initState) @@ -70,7 +69,7 @@ class PreferencesController { return this.setFeatureFlag(key, value) } } -// PUBLIC METHODS + // PUBLIC METHODS /** * Sets the {@code forgottenPassword} state property @@ -81,14 +80,6 @@ class PreferencesController { } /** - * Sets the {@code seedWords} seed words - * @param {string|null} seedWords the seed words - */ - setSeedWords (seedWords) { - this.store.updateState({ seedWords }) - } - - /** * Setter for the `useBlockie` property * * @param {boolean} val Whether or not the user prefers blockie indicators @@ -139,9 +130,9 @@ class PreferencesController { * @param {String} type Indicates the type of first time flow - create or import - the user wishes to follow * */ - setFirstTimeFlowType (type) { - this.store.updateState({ firstTimeFlowType: type }) - } + setFirstTimeFlowType (type) { + this.store.updateState({ firstTimeFlowType: type }) + } getSuggestedTokens () { @@ -503,22 +494,22 @@ class PreferencesController { * @returns {Promise<array>} Promise resolving to updated frequentRpcList. * */ - addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { - const rpcList = this.getFrequentRpcListDetail() - const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) - if (index !== -1) { - rpcList.splice(index, 1) - } - if (url !== 'http://localhost:8545') { - let checkedChainId - if (!!chainId && !Number.isNaN(parseInt(chainId))) { - checkedChainId = chainId - } - rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs }) + addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) + if (index !== -1) { + rpcList.splice(index, 1) + } + if (url !== 'http://localhost:8545') { + let checkedChainId + if (!!chainId && !Number.isNaN(parseInt(chainId))) { + checkedChainId = chainId } - this.store.updateState({ frequentRpcListDetail: rpcList }) - return Promise.resolve(rpcList) + rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs }) } + this.store.updateState({ frequentRpcListDetail: rpcList }) + return Promise.resolve(rpcList) + } /** * Removes custom RPC url from state. @@ -613,12 +604,11 @@ class PreferencesController { return Promise.resolve(true) } - /** - * Sets the {@code completedUiMigration} state to {@code true}, indicating that the user has completed the UI switch. - */ - completeUiMigration () { - this.store.updateState({ completedUiMigration: true }) - return Promise.resolve(true) + unsetMigratedPrivacyMode () { + this.store.updateState({ + migratedPrivacyMode: false, + }) + return Promise.resolve() } // diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 06c499780..5d565c385 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -18,12 +18,13 @@ class ProviderApprovalController extends SafeEventEmitter { */ constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) { super() - this.approvedOrigins = {} this.closePopup = closePopup this.keyringController = keyringController this.openPopup = openPopup this.preferencesController = preferencesController this.store = new ObservableStore({ + approvedOrigins: {}, + dismissedOrigins: {}, providerRequests: [], }) } @@ -45,7 +46,7 @@ class ProviderApprovalController extends SafeEventEmitter { } // register the provider request const metadata = await getSiteMetadata(origin) - this._handleProviderRequest(origin, metadata.name, metadata.icon, false, null) + this._handleProviderRequest(origin, metadata.name, metadata.icon) // wait for resolution of request const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) if (approved) { @@ -63,10 +64,12 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} siteTitle - The title of the document requesting full provider access * @param {string} siteImage - The icon of the window requesting full provider access */ - _handleProviderRequest (origin, siteTitle, siteImage, force, tabID) { - this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] }) + _handleProviderRequest (origin, siteTitle, siteImage) { + this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked - if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { + const { approvedOrigins, dismissedOrigins } = this.store.getState() + const originAlreadyHandled = approvedOrigins[origin] || dismissedOrigins[origin] + if (originAlreadyHandled && this.caching && isUnlocked) { return } this.openPopup && this.openPopup() @@ -78,11 +81,27 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} origin - origin of the domain that had provider access approved */ approveProviderRequestByOrigin (origin) { - this.closePopup && this.closePopup() - const requests = this.store.getState().providerRequests - const providerRequests = requests.filter(request => request.origin !== origin) - this.store.updateState({ providerRequests }) - this.approvedOrigins[origin] = true + if (this.closePopup) { + this.closePopup() + } + + const { approvedOrigins, dismissedOrigins, providerRequests } = this.store.getState() + + let _dismissedOrigins = dismissedOrigins + if (dismissedOrigins[origin]) { + _dismissedOrigins = Object.assign({}, dismissedOrigins) + delete _dismissedOrigins[origin] + } + + const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) + this.store.updateState({ + approvedOrigins: { + ...approvedOrigins, + [origin]: true, + }, + dismissedOrigins: _dismissedOrigins, + providerRequests: remainingProviderRequests, + }) this.emit(`resolvedRequest:${origin}`, { approved: true }) } @@ -92,19 +111,62 @@ class ProviderApprovalController extends SafeEventEmitter { * @param {string} origin - origin of the domain that had provider access approved */ rejectProviderRequestByOrigin (origin) { - this.closePopup && this.closePopup() - const requests = this.store.getState().providerRequests - const providerRequests = requests.filter(request => request.origin !== origin) - this.store.updateState({ providerRequests }) - delete this.approvedOrigins[origin] + if (this.closePopup) { + this.closePopup() + } + + const { approvedOrigins, providerRequests, dismissedOrigins } = this.store.getState() + const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) + + // We're cloning and deleting keys here because we don't want to keep unneeded keys + const _approvedOrigins = Object.assign({}, approvedOrigins) + delete _approvedOrigins[origin] + + this.store.putState({ + approvedOrigins: _approvedOrigins, + providerRequests: remainingProviderRequests, + dismissedOrigins: { + ...dismissedOrigins, + [origin]: true, + }, + }) this.emit(`resolvedRequest:${origin}`, { approved: false }) } /** + * Silently approves access to a full Ethereum provider API for the origin + * + * @param {string} origin - origin of the domain that had provider access approved + */ + forceApproveProviderRequestByOrigin (origin) { + const { approvedOrigins, dismissedOrigins, providerRequests } = this.store.getState() + const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin) + + let _dismissedOrigins = dismissedOrigins + if (dismissedOrigins[origin]) { + _dismissedOrigins = Object.assign({}, dismissedOrigins) + delete _dismissedOrigins[origin] + } + + this.store.updateState({ + approvedOrigins: { + ...approvedOrigins, + [origin]: true, + }, + dismissedOrigins: _dismissedOrigins, + providerRequests: remainingProviderRequests, + }) + + this.emit(`forceResolvedRequest:${origin}`, { approved: true, forced: true }) + } + + /** * Clears any cached approvals for user-approved origins */ clearApprovedOrigins () { - this.approvedOrigins = {} + this.store.updateState({ + approvedOrigins: {}, + }) } /** @@ -115,8 +177,7 @@ class ProviderApprovalController extends SafeEventEmitter { */ shouldExposeAccounts (origin) { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - const result = !privacyMode || Boolean(this.approvedOrigins[origin]) - return result + return !privacyMode || Boolean(this.store.getState().approvedOrigins[origin]) } } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index c4371c25b..a33b46851 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -129,7 +129,7 @@ class TransactionController extends EventEmitter { } } -/** + /** Adds a tx to the txlist @emits ${txMeta.id}:unapproved */ @@ -220,7 +220,7 @@ class TransactionController extends EventEmitter { return txMeta } -/** + /** adds the tx gas defaults: gas && gasPrice @param txMeta {Object} - the txMeta object @returns {Promise<object>} resolves with txMeta @@ -495,9 +495,9 @@ class TransactionController extends EventEmitter { this.txStateManager.updateTx(txMeta, 'transactions#setTxHash') } -// -// PRIVATE METHODS -// + // + // PRIVATE METHODS + // /** maps methods for convenience*/ _mapMethods () { /** @returns the state in transaction controller */ @@ -537,14 +537,14 @@ class TransactionController extends EventEmitter { loadingDefaults: true, }).forEach((tx) => { this.addTxGasDefaults(tx) - .then((txMeta) => { - txMeta.loadingDefaults = false - this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') - }).catch((error) => { - tx.loadingDefaults = false - this.txStateManager.updateTx(tx, 'failed to estimate gas during boot cleanup.') - this.txStateManager.setTxStatusFailed(tx.id, error) - }) + .then((txMeta) => { + txMeta.loadingDefaults = false + this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') + }).catch((error) => { + tx.loadingDefaults = false + this.txStateManager.updateTx(tx, 'failed to estimate gas during boot cleanup.') + this.txStateManager.setTxStatusFailed(tx.id, error) + }) }) this.txStateManager.getFilteredTxList({ diff --git a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 4562568e9..76fc5c35b 100644 --- a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -17,10 +17,10 @@ function migrateFromSnapshotsToDiffs (longHistory) { return ( longHistory // convert non-initial history entries into diffs - .map((entry, index) => { - if (index === 0) return entry - return generateHistoryEntry(longHistory[index - 1], entry) - }) + .map((entry, index) => { + if (index === 0) return entry + return generateHistoryEntry(longHistory[index - 1], entry) + }) ) } diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index 5a8a0cefe..0d2ddddef 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -26,7 +26,7 @@ const normalizers = { gasPrice: gasPrice => addHexPrefix(gasPrice), } - /** +/** normalizes txParams @param txParams {object} @returns {object} normalized txParams @@ -40,7 +40,7 @@ function normalizeTxParams (txParams, LowerCase) { return normalizedTxParams } - /** +/** validates txParams @param txParams {object} */ @@ -59,7 +59,7 @@ function validateTxParams (txParams) { } } - /** +/** validates the from field in txParams @param txParams {object} */ @@ -68,7 +68,7 @@ function validateFrom (txParams) { if (!isValidAddress(txParams.from)) throw new Error('Invalid from address') } - /** +/** validates the to field in txParams @param txParams {object} */ @@ -85,7 +85,7 @@ function validateRecipient (txParams) { return txParams } - /** +/** @returns an {array} of states that can be considered final */ function getFinalStates () { diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index bc11f6633..1ef3be36e 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -186,7 +186,7 @@ class PendingTransactionTracker extends EventEmitter { this.emit('tx:warning', txMeta, err) } } - /** + /** checks to see if if the tx's nonce has been used by another transaction @param txMeta {Object} - txMeta object @emits tx:dropped @@ -198,7 +198,7 @@ class PendingTransactionTracker extends EventEmitter { const nextNonce = await this.query.getTransactionCount(from) const { blockNumber } = await this.query.getTransactionByHash(hash) || {} if (!blockNumber && parseInt(nextNonce) > parseInt(nonce)) { - return true + return true } return false } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 2aa28c270..a91b59918 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -34,7 +34,7 @@ class TransactionStateManager extends EventEmitter { this.store = new ObservableStore( extend({ transactions: [], - }, initState)) + }, initState)) this.txHistoryLimit = txHistoryLimit this.getNetwork = getNetwork } @@ -245,7 +245,7 @@ class TransactionStateManager extends EventEmitter { }) } -/** + /** @param opts {object} - an object of fields to search for eg:<br> let <code>thingsToLookFor = {<br> to: '0x0..',<br> @@ -403,9 +403,9 @@ class TransactionStateManager extends EventEmitter { // Update state this._saveTxList(otherAccountTxs) } -// -// PRIVATE METHODS -// + // + // PRIVATE METHODS + // // STATUS METHODS // statuses: diff --git a/app/scripts/edge-encryptor.js b/app/scripts/edge-encryptor.js index dcb06873b..012672ed2 100644 --- a/app/scripts/edge-encryptor.js +++ b/app/scripts/edge-encryptor.js @@ -14,23 +14,23 @@ class EdgeEncryptor { * @returns {Promise<string>} Promise resolving to an object with ciphertext */ encrypt (password, dataObject) { - var salt = this._generateSalt() - return this._keyFromPassword(password, salt) - .then(function (key) { - var data = JSON.stringify(dataObject) - var dataBuffer = Unibabel.utf8ToBuffer(data) - var vector = global.crypto.getRandomValues(new Uint8Array(16)) - var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector) + var salt = this._generateSalt() + return this._keyFromPassword(password, salt) + .then(function (key) { + var data = JSON.stringify(dataObject) + var dataBuffer = Unibabel.utf8ToBuffer(data) + var vector = global.crypto.getRandomValues(new Uint8Array(16)) + var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector) - var buffer = new Uint8Array(resultbuffer) - var vectorStr = Unibabel.bufferToBase64(vector) - var vaultStr = Unibabel.bufferToBase64(buffer) - return JSON.stringify({ - data: vaultStr, - iv: vectorStr, - salt: salt, - }) - }) + var buffer = new Uint8Array(resultbuffer) + var vectorStr = Unibabel.bufferToBase64(vector) + var vaultStr = Unibabel.bufferToBase64(buffer) + return JSON.stringify({ + data: vaultStr, + iv: vectorStr, + salt: salt, + }) + }) } /** @@ -41,25 +41,25 @@ class EdgeEncryptor { * @returns {Promise<Object>} 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) => { - var 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) - }) - }) + 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) => { + var 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) + }) + }) } /** @@ -72,12 +72,14 @@ class EdgeEncryptor { */ _keyFromPassword (password, salt) { - var passBuffer = Unibabel.utf8ToBuffer(password) - var saltBuffer = Unibabel.base64ToBuffer(salt) - return new Promise((resolve) => { - var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000) - resolve(key) - }) + var passBuffer = Unibabel.utf8ToBuffer(password) + var saltBuffer = Unibabel.base64ToBuffer(salt) + const iterations = 10000 + const length = 32 // SHA256 hash size + return new Promise((resolve) => { + var key = asmcrypto.Pbkdf2HmacSha256(passBuffer, saltBuffer, iterations, length) + resolve(key) + }) } /** @@ -87,10 +89,10 @@ class EdgeEncryptor { * @returns {string} Randomized base64 encoded data */ _generateSalt (byteCount = 32) { - var view = new Uint8Array(byteCount) - global.crypto.getRandomValues(view) - var b64encoded = btoa(String.fromCharCode.apply(null, view)) - return b64encoded + var view = new Uint8Array(byteCount) + global.crypto.getRandomValues(view) + var b64encoded = btoa(String.fromCharCode.apply(null, view)) + return b64encoded } } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index fb16dd43d..a94787b05 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -1,4 +1,36 @@ /*global Web3*/ + + +// need to make sure we aren't affected by overlapping namespaces +// and that we dont affect the app with our namespace +// mostly a fix for web3's BigNumber if AMD's "define" is defined... +let __define + +/** + * Caches reference to global define object and deletes it to + * avoid conflicts with other global define objects, such as + * AMD's define function + */ +const cleanContextForImports = () => { + __define = global.define + try { + global.define = undefined + } catch (_) { + console.warn('MetaMask - global.define could not be deleted.') + } +} + +/** + * Restores global define object from cached reference + */ +const restoreContextAfterImports = () => { + try { + global.define = __define + } catch (_) { + console.warn('MetaMask - global.define could not be overwritten.') + } +} + cleanContextForImports() require('web3/dist/web3.min.js') const log = require('loglevel') @@ -46,6 +78,20 @@ inpageProvider.enable = function ({ force } = {}) { // this will be default true so it does not break any old apps. inpageProvider.autoRefreshOnNetworkChange = true + +// publicConfig isn't populated until we get a message from background. +// Using this getter will ensure the state is available +const getPublicConfigWhenReady = async () => { + const store = inpageProvider.publicConfigStore + let state = store.getState() + // if state is missing, wait for first update + if (!state.networkVersion) { + state = await new Promise(resolve => store.once('update', resolve)) + console.log('new state', state) + } + return state +} + // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** @@ -87,21 +133,8 @@ inpageProvider._metamask = new Proxy({ }, }) -// publicConfig isn't populated until we get a message from background. -// Using this getter will ensure the state is available -async function getPublicConfigWhenReady () { - const store = inpageProvider.publicConfigStore - let state = store.getState() - // if state is missing, wait for first update - if (!state.networkVersion) { - state = await new Promise(resolve => store.once('update', resolve)) - console.log('new state', state) - } - return state -} - // Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound -// `sendAsync` method on the prototype, causing `this` reference issues with drizzle +// `sendAsync` method on the prototype, causing `this` reference issues const proxiedInpageProvider = new Proxy(inpageProvider, { // straight up lie that we deleted the property so that it doesnt // throw an error in strict mode @@ -161,33 +194,3 @@ inpageProvider.publicConfigStore.subscribe(function (state) { window.postMessage('onboardingcomplete', '*') } }) - -// need to make sure we aren't affected by overlapping namespaces -// and that we dont affect the app with our namespace -// mostly a fix for web3's BigNumber if AMD's "define" is defined... -let __define - -/** - * Caches reference to global define object and deletes it to - * avoid conflicts with other global define objects, such as - * AMD's define function - */ -function cleanContextForImports () { - __define = global.define - try { - global.define = undefined - } catch (_) { - console.warn('MetaMask - global.define could not be deleted.') - } -} - -/** - * Restores global define object from cached reference - */ -function restoreContextAfterImports () { - try { - global.define = __define - } catch (_) { - console.warn('MetaMask - global.define could not be overwritten.') - } -} diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 24c5ef7ee..1cbf867cb 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -183,23 +183,23 @@ class AccountTracker { switch (currentNetwork) { case MAINNET_CODE: - await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS) - break + await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS) + break case RINKEYBY_CODE: - await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY) - break + await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY) + break case ROPSTEN_CODE: - await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN) - break + await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN) + break case KOVAN_CODE: - await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN) - break + await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN) + break default: - await Promise.all(addresses.map(this._updateAccount.bind(this))) + await Promise.all(addresses.map(this._updateAccount.bind(this))) } } diff --git a/app/scripts/lib/backend-metametrics.js b/app/scripts/lib/backend-metametrics.js index e3c163c1a..ad7874ead 100644 --- a/app/scripts/lib/backend-metametrics.js +++ b/app/scripts/lib/backend-metametrics.js @@ -15,11 +15,11 @@ function backEndMetaMetricsEvent (metaMaskState, eventData) { const stateEventData = getMetaMetricState({ metamask: metaMaskState }) if (stateEventData.participateInMetaMetrics) { - sendMetaMetricsEvent({ - ...stateEventData, - ...eventData, - url: METAMETRICS_TRACKING_URL + '/backend', - }) + sendMetaMetricsEvent({ + ...stateEventData, + ...eventData, + url: METAMETRICS_TRACKING_URL + '/backend', + }) } } diff --git a/app/scripts/lib/createStreamSink.js b/app/scripts/lib/createStreamSink.js index b93dbc089..eb9d0bd1a 100644 --- a/app/scripts/lib/createStreamSink.js +++ b/app/scripts/lib/createStreamSink.js @@ -1,13 +1,6 @@ const WritableStream = require('readable-stream').Writable const promiseToCallback = require('promise-to-callback') -module.exports = createStreamSink - - -function createStreamSink (asyncWriteFn, _opts) { - return new AsyncWritableStream(asyncWriteFn, _opts) -} - class AsyncWritableStream extends WritableStream { constructor (asyncWriteFn, _opts) { @@ -22,3 +15,9 @@ class AsyncWritableStream extends WritableStream { } } + +function createStreamSink (asyncWriteFn, _opts) { + return new AsyncWritableStream(asyncWriteFn, _opts) +} + +module.exports = createStreamSink diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 82679db5d..86f3e7d47 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -51,6 +51,8 @@ function setupEnsIpfsResolver ({ provider }) { } } else if (type === 'swarm-ns') { url = `https://swarm-gateways.net/bzz:/${hash}${path}${search || ''}` + } else if (type === 'onion' || type === 'onion3') { + url = `http://${hash}.onion${path}${search || ''}` } } catch (err) { console.warn(err) diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index ac41de523..898378389 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -61,7 +61,7 @@ module.exports = class MessageManager extends EventEmitter { */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') - .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) } /** diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 7c13e521a..b5e28be13 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -64,7 +64,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') - .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) } /** diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0b6f5fcb5..d999bb790 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -28,6 +28,7 @@ const PreferencesController = require('./controllers/preferences') const AppStateController = require('./controllers/app-state') const InfuraController = require('./controllers/infura') const CachedBalancesController = require('./controllers/cached-balances') +const OnboardingController = require('./controllers/onboarding') const RecentBlocksController = require('./controllers/recent-blocks') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') @@ -68,7 +69,7 @@ module.exports = class MetamaskController extends EventEmitter { * @constructor * @param {Object} opts */ - constructor (opts) { + constructor (opts) { super() this.defaultMaxListeners = 20 @@ -158,6 +159,10 @@ module.exports = class MetamaskController extends EventEmitter { initState: initState.CachedBalancesController, }) + this.onboardingController = new OnboardingController({ + initState: initState.OnboardingController, + }) + // ensure accountTracker updates balances after network change this.networkController.on('networkDidChange', () => { this.accountTracker._updateAccounts() @@ -262,6 +267,7 @@ module.exports = class MetamaskController extends EventEmitter { NetworkController: this.networkController.store, InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, + OnboardingController: this.onboardingController.store, }) this.memStore = new ComposableObservableStore(null, { @@ -283,6 +289,7 @@ module.exports = class MetamaskController extends EventEmitter { ShapeshiftController: this.shapeshiftController, InfuraController: this.infuraController.store, ProviderApprovalController: this.providerApprovalController.store, + OnboardingController: this.onboardingController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -362,9 +369,9 @@ module.exports = class MetamaskController extends EventEmitter { return publicConfigStore } -//============================================================================= -// EXPOSED TO THE UI SUBSYSTEM -//============================================================================= + //============================================================================= + // EXPOSED TO THE UI SUBSYSTEM + //============================================================================= /** * The metamask-state of the various controllers, made available to the UI @@ -398,6 +405,7 @@ module.exports = class MetamaskController extends EventEmitter { const txController = this.txController const networkController = this.networkController const providerApprovalController = this.providerApprovalController + const onboardingController = this.onboardingController return { // etc @@ -420,9 +428,7 @@ module.exports = class MetamaskController extends EventEmitter { // primary HD keyring management addNewAccount: nodeify(this.addNewAccount, this), - placeSeedWords: this.placeSeedWords.bind(this), verifySeedPhrase: nodeify(this.verifySeedPhrase, this), - clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), removeAccount: nodeify(this.removeAccount, this), importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this), @@ -454,15 +460,16 @@ module.exports = class MetamaskController extends EventEmitter { setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), setPreference: nodeify(preferencesController.setPreference, preferencesController), - completeUiMigration: nodeify(preferencesController.completeUiMigration, preferencesController), completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController), addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController), + unsetMigratedPrivacyMode: nodeify(preferencesController.unsetMigratedPrivacyMode, preferencesController), // BlacklistController whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), // AddressController setAddressBook: this.addressBookController.set.bind(this.addressBookController), + removeFromAddressBook: this.addressBookController.delete.bind(this.addressBookController), // AppStateController setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), @@ -500,14 +507,18 @@ module.exports = class MetamaskController extends EventEmitter { // provider approval approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), + forceApproveProviderRequestByOrigin: providerApprovalController.forceApproveProviderRequestByOrigin.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), + + // onboarding controller + setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController), } } -//============================================================================= -// VAULT / KEYRING RELATED METHODS -//============================================================================= + //============================================================================= + // VAULT / KEYRING RELATED METHODS + //============================================================================= /** * Creates a new Vault and create a new keychain. @@ -617,7 +628,7 @@ module.exports = class MetamaskController extends EventEmitter { * with the mobile client for syncing purposes * @returns Promise<Object> Parts of the state that we want to syncx */ - async fetchInfoToSync () { + async fetchInfoToSync () { // Preferences const { accountTokens, @@ -746,14 +757,14 @@ module.exports = class MetamaskController extends EventEmitter { const keyring = await this.getKeyringForDevice(deviceName, hdPath) let accounts = [] switch (page) { - case -1: - accounts = await keyring.getPreviousPage() - break - case 1: - accounts = await keyring.getNextPage() - break - default: - accounts = await keyring.getFirstPage() + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() } // Merge with existing accounts @@ -810,7 +821,7 @@ module.exports = class MetamaskController extends EventEmitter { const { identities } = this.preferencesController.store.getState() return { ...keyState, identities } - } + } // @@ -846,26 +857,6 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Adds the current vault's seed words to the UI's state tree. - * - * Used when creating a first vault, to allow confirmation. - * Also used when revealing the seed words in the confirmation view. - * - * @param {Function} cb - A callback called on completion. - */ - placeSeedWords (cb) { - - this.verifySeedPhrase() - .then((seedWords) => { - this.preferencesController.setSeedWords(seedWords) - return cb(null, seedWords) - }) - .catch((err) => { - return cb(err) - }) - } - - /** * Verifies the validity of the current vault's seed phrase. * * Validity: seed phrase restores the accounts belonging to the current vault. @@ -899,18 +890,6 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Remove the primary account seed phrase from the UI's state tree. - * - * The seed phrase remains available in the background process. - * - * @param {function} cb Callback function called with the current address. - */ - clearSeedWordCache (cb) { - this.preferencesController.setSeedWords(null) - cb(null, this.preferencesController.getSelectedAddress()) - } - - /** * Clears the transaction history, to allow users to force-reset their nonces. * Mostly used in development environments, when networks are restarted with * the same network ID. @@ -1009,16 +988,16 @@ module.exports = class MetamaskController extends EventEmitter { // sets the status op the message to 'approved' // and removes the metamaskId for signing return this.messageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { + .then((cleanMsgParams) => { // signs the message - return this.keyringController.signMessage(cleanMsgParams) - }) - .then((rawSig) => { + return this.keyringController.signMessage(cleanMsgParams) + }) + .then((rawSig) => { // tells the listener that the message has been signed // and can be returned to the dapp - this.messageManager.setMsgStatusSigned(msgId, rawSig) - return this.getState() - }) + this.messageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) } /** @@ -1067,16 +1046,16 @@ module.exports = class MetamaskController extends EventEmitter { // sets the status op the message to 'approved' // and removes the metamaskId for signing return this.personalMessageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { + .then((cleanMsgParams) => { // signs the message - return this.keyringController.signPersonalMessage(cleanMsgParams) - }) - .then((rawSig) => { + return this.keyringController.signPersonalMessage(cleanMsgParams) + }) + .then((rawSig) => { // tells the listener that the message has been signed // and can be returned to the dapp - this.personalMessageManager.setMsgStatusSigned(msgId, rawSig) - return this.getState() - }) + this.personalMessageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) } /** @@ -1176,7 +1155,7 @@ module.exports = class MetamaskController extends EventEmitter { restoreOldVaultAccounts (migratorOutput) { const { serialized } = migratorOutput return this.keyringController.restoreKeyring(serialized) - .then(() => migratorOutput) + .then(() => migratorOutput) } /** @@ -1219,9 +1198,9 @@ module.exports = class MetamaskController extends EventEmitter { }) } -//============================================================================= -// END (VAULT / KEYRING RELATED METHODS) -//============================================================================= + //============================================================================= + // END (VAULT / KEYRING RELATED METHODS) + //============================================================================= /** * Allows a user to try to speed up a transaction by retrying it @@ -1270,9 +1249,9 @@ module.exports = class MetamaskController extends EventEmitter { }) } -//============================================================================= -// PASSWORD MANAGEMENT -//============================================================================= + //============================================================================= + // PASSWORD MANAGEMENT + //============================================================================= /** * Allows a user to begin the seed phrase recovery process. @@ -1294,9 +1273,9 @@ module.exports = class MetamaskController extends EventEmitter { cb() } -//============================================================================= -// SETUP -//============================================================================= + //============================================================================= + // SETUP + //============================================================================= /** * Used to create a multiplexed stream for connecting to an untrusted context @@ -1319,6 +1298,8 @@ module.exports = class MetamaskController extends EventEmitter { const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi) this.setupPublicConfig(mux.createStream('publicConfig'), originDomain) + + this.providerApprovalController.on(`forceResolvedRequest:${originDomain}`, publicApi.forceReloadSite) } /** @@ -1392,6 +1373,32 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} origin - The URI of the requesting resource. */ setupProviderConnection (outStream, origin, publicApi) { + const getSiteMetadata = publicApi && publicApi.getSiteMetadata + const engine = this.setupProviderEngine(origin, getSiteMetadata) + + // setup connection + const providerStream = createEngineStream({ engine }) + + pump( + outStream, + providerStream, + outStream, + (err) => { + // cleanup filter polyfill middleware + engine._middleware.forEach((mid) => { + if (mid.destroy && typeof mid.destroy === 'function') { + mid.destroy() + } + }) + if (err) log.error(err) + } + ) + } + + /** + * A method for creating a provider that is safely restricted for the requesting domain. + **/ + setupProviderEngine (origin, getSiteMetadata) { // setup json rpc engine stack const engine = new RpcEngine() const provider = this.provider @@ -1399,6 +1406,7 @@ module.exports = class MetamaskController extends EventEmitter { // create filter polyfill middleware const filterMiddleware = createFilterMiddleware({ provider, blockTracker }) + // create subscription polyfill middleware const subscriptionManager = createSubscriptionManager({ provider, blockTracker }) subscriptionManager.events.on('notification', (message) => engine.emit('notification', message)) @@ -1414,24 +1422,11 @@ module.exports = class MetamaskController extends EventEmitter { // requestAccounts engine.push(this.providerApprovalController.createMiddleware({ origin, - getSiteMetadata: publicApi && publicApi.getSiteMetadata, + getSiteMetadata, })) // forward to metamask primary provider engine.push(providerAsMiddleware(provider)) - - // setup connection - const providerStream = createEngineStream({ engine }) - - pump( - outStream, - providerStream, - outStream, - (err) => { - // cleanup filter polyfill middleware - filterMiddleware.destroy() - if (err) log.error(err) - } - ) + return engine } /** @@ -1485,6 +1480,10 @@ module.exports = class MetamaskController extends EventEmitter { const publicApi = { // wrap with an await remote + forceReloadSite: async () => { + const remote = await getRemote() + return await pify(remote.forceReloadSite)() + }, getSiteMetadata: async () => { const remote = await getRemote() return await pify(remote.getSiteMetadata)() @@ -1551,13 +1550,13 @@ module.exports = class MetamaskController extends EventEmitter { return GWEI_BN } return block.gasPrices - .map(hexPrefix => hexPrefix.substr(2)) - .map(hex => new BN(hex, 16)) - .sort((a, b) => { - return a.gt(b) ? 1 : -1 - })[0] + .map(hexPrefix => hexPrefix.substr(2)) + .map(hex => new BN(hex, 16)) + .sort((a, b) => { + return a.gt(b) ? 1 : -1 + })[0] }) - .map(number => number.div(GWEI_BN).toNumber()) + .map(number => number.div(GWEI_BN).toNumber()) const percentileNum = percentile(65, lowestPrices) const percentileNumBn = new BN(percentileNum) @@ -1577,9 +1576,9 @@ module.exports = class MetamaskController extends EventEmitter { return pendingNonce } -//============================================================================= -// CONFIG -//============================================================================= + //============================================================================= + // CONFIG + //============================================================================= // Log blocks @@ -1774,7 +1773,7 @@ module.exports = class MetamaskController extends EventEmitter { this.tokenRatesController.isActive = active } - /** + /** * Creates RPC engine middleware for processing eth_signTypedData requests * * @param {Object} req - request object @@ -1798,3 +1797,4 @@ module.exports = class MetamaskController extends EventEmitter { return this.keyringController.setLocked() } } + diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js index ffbf24a4b..f6442dacc 100644 --- a/app/scripts/migrations/018.js +++ b/app/scripts/migrations/018.js @@ -43,9 +43,9 @@ function transformState (state) { const newHistory = ( txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history) // remove empty diffs - .filter((entry) => { - return !Array.isArray(entry) || entry.length > 0 - }) + .filter((entry) => { + return !Array.isArray(entry) || entry.length > 0 + }) ) txMeta.history = newHistory return txMeta diff --git a/app/scripts/migrations/019.js b/app/scripts/migrations/019.js index ce5da6859..7b726c3e8 100644 --- a/app/scripts/migrations/019.js +++ b/app/scripts/migrations/019.js @@ -38,13 +38,13 @@ function transformState (state) { if (txMeta.status !== 'submitted') return txMeta const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed') - .filter((tx) => tx.txParams.from === txMeta.txParams.from) - .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from) + .filter((tx) => tx.txParams.from === txMeta.txParams.from) + .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from) const highestConfirmedNonce = getHighestNonce(confirmedTxs) const pendingTxs = txList.filter((tx) => tx.status === 'submitted') - .filter((tx) => tx.txParams.from === txMeta.txParams.from) - .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from) + .filter((tx) => tx.txParams.from === txMeta.txParams.from) + .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from) const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce) const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce) @@ -78,7 +78,7 @@ function getHighestContinuousFrom (txList, startPoint) { function getHighestNonce (txList) { const nonces = txList.map((txMeta) => { - const nonce = txMeta.txParams.nonce + const nonce = txMeta.txParams.nonce return parseInt(nonce || '0x0', 16) }) const highestNonce = Math.max.apply(null, nonces) diff --git a/app/scripts/migrations/024.js b/app/scripts/migrations/024.js index 6239bab13..5ffaea377 100644 --- a/app/scripts/migrations/024.js +++ b/app/scripts/migrations/024.js @@ -32,7 +32,7 @@ function transformState (state) { txMeta.status === 'unapproved' && txMeta.txParams && txMeta.txParams.from - ) { + ) { txMeta.txParams.from = txMeta.txParams.from.toLowerCase() } return txMeta diff --git a/app/scripts/migrations/031.js b/app/scripts/migrations/031.js index 98d182828..9c8cbeb09 100644 --- a/app/scripts/migrations/031.js +++ b/app/scripts/migrations/031.js @@ -2,14 +2,14 @@ const version = 31 const clone = require('clone') - /* +/* * The purpose of this migration is to properly set the completedOnboarding flag baesd on the state * of the KeyringController. */ module.exports = { version, - migrate: async function (originalVersionedData) { + migrate: async function (originalVersionedData) { const versionedData = clone(originalVersionedData) versionedData.meta.version = version const state = versionedData.data @@ -19,13 +19,13 @@ module.exports = { }, } - function transformState (state) { +function transformState (state) { const { KeyringController, PreferencesController } = state - if (KeyringController && PreferencesController) { + if (KeyringController && PreferencesController) { const { vault } = KeyringController PreferencesController.completedOnboarding = Boolean(vault) } - return state + return state } diff --git a/app/scripts/migrations/034.js b/app/scripts/migrations/034.js new file mode 100644 index 000000000..7c852de96 --- /dev/null +++ b/app/scripts/migrations/034.js @@ -0,0 +1,33 @@ +const version = 34 +const clone = require('clone') + +/** + * The purpose of this migration is to enable the {@code privacyMode} feature flag and set the user as being migrated + * if it was {@code false}. + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + const { PreferencesController } = state + + if (PreferencesController) { + const featureFlags = PreferencesController.featureFlags || {} + + if (!featureFlags.privacyMode && typeof PreferencesController.migratedPrivacyMode === 'undefined') { + // Mark the state has being migrated and enable Privacy Mode + PreferencesController.migratedPrivacyMode = true + featureFlags.privacyMode = true + } + } + + return state +} diff --git a/app/scripts/migrations/035.js b/app/scripts/migrations/035.js new file mode 100644 index 000000000..02b01f588 --- /dev/null +++ b/app/scripts/migrations/035.js @@ -0,0 +1,28 @@ +// next version number +const version = 35 + +/* + +Removes the deprecated 'seedWords' state + +*/ + +const clone = require('clone') + +module.exports = { + version, + + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + versionedData.data = transformState(versionedData.data) + return versionedData + }, +} + +function transformState (state) { + if (state.PreferencesController && state.PreferencesController.seedWords !== undefined) { + delete state.PreferencesController.seedWords + } + return state +} diff --git a/app/scripts/phishing-detect.js b/app/scripts/phishing-detect.js index b30c76b72..266e4fc31 100644 --- a/app/scripts/phishing-detect.js +++ b/app/scripts/phishing-detect.js @@ -37,11 +37,11 @@ function start () { function setupControllerConnection (connectionStream, cb) { const eventEmitter = new EventEmitter() - const accountManagerDnode = dnode({ + const metaMaskControllerDnode = dnode({ sendUpdate (state) { eventEmitter.emit('update', state) }, }) - connectionStream.pipe(accountManagerDnode).pipe(connectionStream) - accountManagerDnode.once('remote', (accountManager) => cb(null, accountManager)) + connectionStream.pipe(metaMaskControllerDnode).pipe(connectionStream) + metaMaskControllerDnode.once('remote', (backgroundConnection) => cb(null, backgroundConnection)) } diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 0c2d222b8..43820515d 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -68,7 +68,7 @@ class ExtensionPlatform { const nonce = parseInt(txMeta.txParams.nonce, 16) const title = 'Confirmed transaction' - const message = `Transaction ${nonce} confirmed! View on EtherScan` + const message = `Transaction ${nonce} confirmed! View on Etherscan` this._showNotification(title, message, url) } @@ -84,20 +84,20 @@ class ExtensionPlatform { extension.notifications.create( url, { - 'type': 'basic', - 'title': title, - 'iconUrl': extension.extension.getURL('../../images/icon-64.png'), - 'message': message, + 'type': 'basic', + 'title': title, + 'iconUrl': extension.extension.getURL('../../images/icon-64.png'), + 'message': message, }) } _subscribeToNotificationClicked () { - if (!extension.notifications.onClicked.hasListener(this._viewOnEtherScan)) { - extension.notifications.onClicked.addListener(this._viewOnEtherScan) + if (!extension.notifications.onClicked.hasListener(this._viewOnEtherscan)) { + extension.notifications.onClicked.addListener(this._viewOnEtherscan) } } - _viewOnEtherScan (txId) { + _viewOnEtherscan (txId) { if (txId.startsWith('http://')) { extension.tabs.create({ url: txId }) } diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js deleted file mode 100644 index e964d001d..000000000 --- a/app/scripts/popup-core.js +++ /dev/null @@ -1,77 +0,0 @@ -const {EventEmitter} = require('events') -const async = require('async') -const Dnode = require('dnode') -const Eth = require('ethjs') -const EthQuery = require('eth-query') -const launchMetamaskUi = require('../../ui') -const StreamProvider = require('web3-stream-provider') -const {setupMultiplex} = require('./lib/stream-utils.js') - -module.exports = initializePopup - -/** - * Asynchronously initializes the MetaMask popup UI - * - * @param {{ container: Element, connectionStream: * }} config Popup configuration object - * @param {Function} cb Called when initialization is complete - */ -function initializePopup ({ container, connectionStream }, cb) { - // setup app - async.waterfall([ - (cb) => connectToAccountManager(connectionStream, cb), - (accountManager, cb) => launchMetamaskUi({ container, accountManager }, cb), - ], cb) -} - -/** - * Establishes streamed connections to background scripts and a Web3 provider - * - * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection - * @param {Function} cb Called when controller connection is established - */ -function connectToAccountManager (connectionStream, cb) { - // setup communication with background - // setup multiplexing - const mx = setupMultiplex(connectionStream) - // connect features - setupControllerConnection(mx.createStream('controller'), cb) - setupWeb3Connection(mx.createStream('provider')) -} - -/** - * Establishes a streamed connection to a Web3 provider - * - * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection - */ -function setupWeb3Connection (connectionStream) { - const providerStream = new StreamProvider() - providerStream.pipe(connectionStream).pipe(providerStream) - connectionStream.on('error', console.error.bind(console)) - providerStream.on('error', console.error.bind(console)) - global.ethereumProvider = providerStream - global.ethQuery = new EthQuery(providerStream) - global.eth = new Eth(providerStream) -} - -/** - * Establishes a streamed connection to the background account manager - * - * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection - * @param {Function} cb Called when the remote account manager connection is established - */ -function setupControllerConnection (connectionStream, cb) { - // this is a really sneaky way of adding EventEmitter api - // to a bi-directional dnode instance - const eventEmitter = new EventEmitter() - const accountManagerDnode = Dnode({ - sendUpdate: function (state) { - eventEmitter.emit('update', state) - }, - }) - connectionStream.pipe(accountManagerDnode).pipe(connectionStream) - accountManagerDnode.once('remote', function (accountManager) { - // setup push events - accountManager.on = eventEmitter.on.bind(eventEmitter) - cb(null, accountManager) - }) -} diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 67ec6bf06..a99da37a0 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -1,14 +1,19 @@ -const injectCss = require('inject-css') -const NewMetaMaskUiCss = require('../../ui/css') -const startPopup = require('./popup-core') const PortStream = require('extension-port-stream') const { getEnvironmentType } = require('./lib/util') -const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN } = require('./lib/enums') +const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_POPUP } = require('./lib/enums') const extension = require('extensionizer') const ExtensionPlatform = require('./platforms/extension') const NotificationManager = require('./lib/notification-manager') const notificationManager = new NotificationManager() const setupSentry = require('./lib/setupSentry') +const {EventEmitter} = require('events') +const Dnode = require('dnode') +const Eth = require('ethjs') +const EthQuery = require('eth-query') +const urlUtil = require('url') +const launchMetaMaskUi = require('../../ui') +const StreamProvider = require('web3-stream-provider') +const {setupMultiplex} = require('./lib/stream-utils.js') const log = require('loglevel') start().catch(log.error) @@ -41,22 +46,8 @@ async function start () { const extensionPort = extension.runtime.connect({ name: windowType }) const connectionStream = new PortStream(extensionPort) - // start ui - const container = document.getElementById('app-content') - startPopup({ container, connectionStream }, (err, store) => { - if (err) return displayCriticalError(err) - - const state = store.getState() - const { metamask: { completedOnboarding } = {} } = state - - if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) { - global.platform.openExtensionInBrowser() - return - } - - injectCss(NewMetaMaskUiCss()) - }) - + const activeTab = await queryCurrentActiveTab(windowType) + initializeUiWithTab(activeTab) function closePopupIfOpen (windowType) { if (windowType !== ENVIRONMENT_TYPE_NOTIFICATION) { @@ -65,11 +56,107 @@ async function start () { } } - function displayCriticalError (err) { + function displayCriticalError (container, err) { container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>' container.style.height = '80px' log.error(err.stack) throw err } + function initializeUiWithTab (tab) { + const container = document.getElementById('app-content') + initializeUi(tab, container, connectionStream, (err, store) => { + if (err) { + return displayCriticalError(container, err) + } + + const state = store.getState() + const { metamask: { completedOnboarding } = {} } = state + + if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) { + global.platform.openExtensionInBrowser() + } + }) + } +} + +async function queryCurrentActiveTab (windowType) { + return new Promise((resolve) => { + // At the time of writing we only have the `activeTab` permission which means + // that this query will only succeed in the popup context (i.e. after a "browserAction") + if (windowType !== ENVIRONMENT_TYPE_POPUP) { + resolve({}) + return + } + + extension.tabs.query({active: true, currentWindow: true}, (tabs) => { + const [activeTab] = tabs + const {title, url} = activeTab + const { hostname: origin, protocol } = url ? urlUtil.parse(url) : {} + resolve({ + title, origin, protocol, url, + }) + }) + }) +} + +function initializeUi (activeTab, container, connectionStream, cb) { + connectToAccountManager(connectionStream, (err, backgroundConnection) => { + if (err) { + return cb(err) + } + + launchMetaMaskUi({ + activeTab, + container, + backgroundConnection, + }, cb) + }) +} + +/** + * Establishes a connection to the background and a Web3 provider + * + * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection + * @param {Function} cb Called when controller connection is established + */ +function connectToAccountManager (connectionStream, cb) { + const mx = setupMultiplex(connectionStream) + setupControllerConnection(mx.createStream('controller'), cb) + setupWeb3Connection(mx.createStream('provider')) +} + +/** + * Establishes a streamed connection to a Web3 provider + * + * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection + */ +function setupWeb3Connection (connectionStream) { + const providerStream = new StreamProvider() + providerStream.pipe(connectionStream).pipe(providerStream) + connectionStream.on('error', console.error.bind(console)) + providerStream.on('error', console.error.bind(console)) + global.ethereumProvider = providerStream + global.ethQuery = new EthQuery(providerStream) + global.eth = new Eth(providerStream) +} + +/** + * Establishes a streamed connection to the background account manager + * + * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection + * @param {Function} cb Called when the remote account manager connection is established + */ +function setupControllerConnection (connectionStream, cb) { + const eventEmitter = new EventEmitter() + const backgroundDnode = Dnode({ + sendUpdate: function (state) { + eventEmitter.emit('update', state) + }, + }) + connectionStream.pipe(backgroundDnode).pipe(connectionStream) + backgroundDnode.once('remote', function (backgroundConnection) { + backgroundConnection.on = eventEmitter.on.bind(eventEmitter) + cb(null, backgroundConnection) + }) } |