diff options
Diffstat (limited to 'app/scripts')
34 files changed, 620 insertions, 982 deletions
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 68b6117e5..0c55ae39f 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,18 +1,17 @@ const fs = require('fs') const path = require('path') const pump = require('pump') +const log = require('loglevel') +const Dnode = require('dnode') const querystring = require('querystring') const LocalMessageDuplexStream = require('post-message-stream') -const PongStream = require('ping-pong-stream/pong') const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('extension-port-stream') -const {Transform: TransformStream} = require('stream') const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' const inpageBundle = inpageContent + inpageSuffix -let isEnabled = false // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -23,9 +22,7 @@ let isEnabled = false if (shouldInjectWeb3()) { injectScript(inpageBundle) - setupStreams() - listenForProviderRequest() - checkPrivacyMode() + start() } /** @@ -47,148 +44,107 @@ function injectScript (content) { } /** + * Sets up the stream communication and submits site metadata + * + */ +async function start () { + await setupStreams() + await domIsReady() +} + +/** * Sets up two-way communication streams between the - * browser extension and local per-page browser context + * browser extension and local per-page browser context. + * */ -function setupStreams () { - // setup communication to page and plugin +async function setupStreams () { + // the transport-specific streams for communication between inpage and background const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) - const pluginPort = extension.runtime.connect({ name: 'contentscript' }) - const pluginStream = new PortStream(pluginPort) + const extensionPort = extension.runtime.connect({ name: 'contentscript' }) + const extensionStream = new PortStream(extensionPort) - // Filter out selectedAddress until this origin is enabled - const approvalTransform = new TransformStream({ - objectMode: true, - transform: (data, _, done) => { - if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !isEnabled) { - data.data.selectedAddress = undefined - } - done(null, { ...data }) - }, - }) + // create and connect channel muxers + // so we can handle the channels individually + const pageMux = new ObjectMultiplex() + pageMux.setMaxListeners(25) + const extensionMux = new ObjectMultiplex() + extensionMux.setMaxListeners(25) - // forward communication plugin->inpage pump( + pageMux, pageStream, - pluginStream, - approvalTransform, - pageStream, - (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) + pageMux, + (err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err) ) - - // setup local multistream channels - const mux = new ObjectMultiplex() - mux.setMaxListeners(25) - pump( - mux, - pageStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask Inpage', err) - ) - pump( - mux, - pluginStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask Background', err) + extensionMux, + extensionStream, + extensionMux, + (err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err) ) - // connect ping stream - const pongStream = new PongStream({ objectMode: true }) - pump( - mux, - pongStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask PingPongStream', err) - ) + // forward communication across inpage-background for these channels only + forwardTrafficBetweenMuxers('provider', pageMux, extensionMux) + forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux) - // connect phishing warning stream - const phishingStream = mux.createStream('phishing') + // connect "phishing" channel to warning system + const phishingStream = extensionMux.createStream('phishing') phishingStream.once('data', redirectToPhishingWarning) - // ignore unused channels (handled by background, inpage) - mux.ignoreStream('provider') - mux.ignoreStream('publicConfig') + // connect "publicApi" channel to submit page metadata + const publicApiStream = extensionMux.createStream('publicApi') + const background = await setupPublicApi(publicApiStream) + + return { background } } -/** - * Establishes listeners for requests to fully-enable the provider from the dapp context - * and for full-provider approvals and rejections from the background script context. Dapps - * should not post messages directly and should instead call provider.enable(), which - * handles posting these messages internally. - */ -function listenForProviderRequest () { - window.addEventListener('message', ({ source, data }) => { - if (source !== window || !data || !data.type) { return } - switch (data.type) { - case 'ETHEREUM_ENABLE_PROVIDER': - extension.runtime.sendMessage({ - action: 'init-provider-request', - force: data.force, - origin: source.location.hostname, - siteImage: getSiteIcon(source), - siteTitle: getSiteName(source), - }) - break - case 'ETHEREUM_IS_APPROVED': - extension.runtime.sendMessage({ - action: 'init-is-approved', - origin: source.location.hostname, - }) - break - case 'METAMASK_IS_UNLOCKED': - extension.runtime.sendMessage({ - action: 'init-is-unlocked', - }) - break - } - }) +function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { + const channelA = muxA.createStream(channelName) + const channelB = muxB.createStream(channelName) + pump( + channelA, + channelB, + channelA, + (err) => logStreamDisconnectWarning(`MetaMask muxed traffic for channel "${channelName}" failed.`, err) + ) +} - extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked, selectedAddress }) => { - switch (action) { - case 'approve-provider-request': - isEnabled = true - window.postMessage({ type: 'ethereumprovider', selectedAddress }, '*') - break - case 'approve-legacy-provider-request': - isEnabled = true - window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*') - break - case 'reject-provider-request': - window.postMessage({ type: 'ethereumprovider', error: 'User denied account authorization' }, '*') - break - case 'answer-is-approved': - window.postMessage({ type: 'ethereumisapproved', isApproved, caching }, '*') - break - case 'answer-is-unlocked': - window.postMessage({ type: 'metamaskisunlocked', isUnlocked }, '*') - break - case 'metamask-set-locked': - isEnabled = false - window.postMessage({ type: 'metamasksetlocked' }, '*') - break - case 'ethereum-ping-success': - window.postMessage({ type: 'ethereumpingsuccess' }, '*') - break - case 'ethereum-ping-error': - window.postMessage({ type: 'ethereumpingerror' }, '*') +async function setupPublicApi (outStream) { + const api = { + getSiteMetadata: (cb) => cb(null, getSiteMetadata()), + } + const dnode = Dnode(api) + pump( + outStream, + dnode, + outStream, + (err) => { + // report any error + if (err) log.error(err) } - }) + ) + const background = await new Promise(resolve => dnode.once('remote', resolve)) + return background } /** - * Checks if MetaMask is currently operating in "privacy mode", meaning - * dapps must call ethereum.enable in order to access user accounts + * Gets site metadata and returns it + * */ -function checkPrivacyMode () { - extension.runtime.sendMessage({ action: 'init-privacy-request' }) +function getSiteMetadata () { + // get metadata + const metadata = { + name: getSiteName(window), + icon: getSiteIcon(window), + } + return metadata } /** - * Error handler for page to plugin stream disconnections + * Error handler for page to extension stream disconnections * * @param {string} remoteLabel Remote stream name * @param {Error} err Stream connection error @@ -275,6 +231,7 @@ function blacklistedDomainCheck () { 'harbourair.com', 'ani.gamer.com.tw', 'blueskybooking.com', + 'sharefile.com', ] const currentUrl = window.location.href let currentRegex @@ -300,6 +257,10 @@ function redirectToPhishingWarning () { })}` } + +/** + * Extracts a name for the site from the DOM + */ function getSiteName (window) { const document = window.document const siteName = document.querySelector('head > meta[property="og:site_name"]') @@ -315,6 +276,9 @@ function getSiteName (window) { return document.title } +/** + * Extracts an icon for the site from the DOM + */ function getSiteIcon (window) { const document = window.document @@ -332,3 +296,13 @@ function getSiteIcon (window) { return null } + +/** + * Returns a promise that resolves when the DOM is loaded (does not wait for images to load) + */ +async function domIsReady () { + // already loaded + if (['interactive', 'complete'].includes(document.readyState)) return + // wait for load + await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) +} diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js new file mode 100644 index 000000000..9533fd458 --- /dev/null +++ b/app/scripts/controllers/app-state.js @@ -0,0 +1,73 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +class AppStateController { + /** + * @constructor + * @param opts + */ + constructor (opts = {}) { + const {initState, onInactiveTimeout, preferencesStore} = opts + const {preferences} = preferencesStore.getState() + + this.onInactiveTimeout = onInactiveTimeout || (() => {}) + this.store = new ObservableStore(extend({ + timeoutMinutes: 0, + }, initState)) + this.timer = null + + preferencesStore.subscribe(state => { + this._setInactiveTimeout(state.preferences.autoLogoutTimeLimit) + }) + + this._setInactiveTimeout(preferences.autoLogoutTimeLimit) + } + + /** + * Sets the last active time to the current time + * @return {void} + */ + setLastActiveTime () { + this._resetTimer() + } + + /** + * Sets the inactive timeout for the app + * @param {number} timeoutMinutes the inactive timeout in minutes + * @return {void} + * @private + */ + _setInactiveTimeout (timeoutMinutes) { + this.store.putState({ + timeoutMinutes, + }) + + this._resetTimer() + } + + /** + * Resets the internal inactive timer + * + * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new + * timer will not be created. + * + * @return {void} + * @private + */ + _resetTimer () { + const {timeoutMinutes} = this.store.getState() + + if (this.timer) { + clearTimeout(this.timer) + } + + if (!timeoutMinutes) { + return + } + + this.timer = setTimeout(() => this.onInactiveTimeout(), timeoutMinutes * 60 * 1000) + } +} + +module.exports = AppStateController + diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 465751e61..b227d5d0a 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -68,7 +68,7 @@ class BalanceController { _registerUpdates () { const update = this.updateBalance.bind(this) - this.txController.on('tx:status-update', (txId, status) => { + this.txController.on('tx:status-update', (_, status) => { switch (status) { case 'submitted': case 'confirmed': diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js deleted file mode 100644 index e55b09d03..000000000 --- a/app/scripts/controllers/blacklist.js +++ /dev/null @@ -1,136 +0,0 @@ -const ObservableStore = require('obs-store') -const extend = require('xtend') -const PhishingDetector = require('eth-phishing-detect/src/detector') -const log = require('loglevel') - -// compute phishing lists -const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') -// every four minutes -const POLLING_INTERVAL = 4 * 60 * 1000 - -class BlacklistController { - - /** - * Responsible for polling for and storing an up to date 'eth-phishing-detect' config.json file, while - * exposing a method that can check whether a given url is a phishing attempt. The 'eth-phishing-detect' - * config.json file contains a fuzzylist, whitelist and blacklist. - * - * - * @typedef {Object} BlacklistController - * @param {object} opts Overrides the defaults for the initial state of this.store - * @property {object} store The the store of the current phishing config - * @property {object} store.phishing Contains fuzzylist, whitelist and blacklist arrays. @see - * {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json} - * @property {object} _phishingDetector The PhishingDetector instantiated by passing store.phishing to - * PhishingDetector. - * @property {object} _phishingUpdateIntervalRef Id of the interval created to periodically update the blacklist - * - */ - constructor (opts = {}) { - const initState = extend({ - phishing: PHISHING_DETECTION_CONFIG, - whitelist: [], - }, opts.initState) - this.store = new ObservableStore(initState) - // phishing detector - this._phishingDetector = null - this._setupPhishingDetector(initState.phishing) - // polling references - this._phishingUpdateIntervalRef = null - } - - /** - * Adds the given hostname to the runtime whitelist - * @param {string} hostname the hostname to whitelist - */ - whitelistDomain (hostname) { - if (!hostname) { - return - } - - const { whitelist } = this.store.getState() - this.store.updateState({ - whitelist: [...new Set([hostname, ...whitelist])], - }) - } - - /** - * Given a url, returns the result of checking if that url is in the store.phishing blacklist - * - * @param {string} hostname The hostname portion of a url; the one that will be checked against the white and - * blacklists of store.phishing - * @returns {boolean} Whether or not the passed hostname is on our phishing blacklist - * - */ - checkForPhishing (hostname) { - if (!hostname) return false - - const { whitelist } = this.store.getState() - if (whitelist.some((e) => e === hostname)) { - return false - } - - const { result } = this._phishingDetector.check(hostname) - return result - } - - /** - * Queries `https://api.infura.io/v2/blacklist` for an updated blacklist config. This is passed to this._phishingDetector - * to update our phishing detector instance, and is updated in the store. The new phishing config is returned - * - * - * @returns {Promise<object>} Promises the updated blacklist config for the phishingDetector - * - */ - async updatePhishingList () { - // make request - let response - try { - response = await fetch('https://api.infura.io/v2/blacklist') - } catch (err) { - log.error(new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`)) - return - } - // parse response - let rawResponse - let phishing - try { - const rawResponse = await response.text() - phishing = JSON.parse(rawResponse) - } catch (err) { - log.error(new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`)) - return - } - // update current blacklist - this.store.updateState({ phishing }) - this._setupPhishingDetector(phishing) - return phishing - } - - /** - * Initiates the updating of the local blacklist at a set interval. The update is done via this.updatePhishingList(). - * Also, this method store a reference to that interval at this._phishingUpdateIntervalRef - * - */ - scheduleUpdates () { - if (this._phishingUpdateIntervalRef) return - this.updatePhishingList() - this._phishingUpdateIntervalRef = setInterval(() => { - this.updatePhishingList() - }, POLLING_INTERVAL) - } - - /** - * Sets this._phishingDetector to a new PhishingDetector instance. - * @see {@link https://github.com/MetaMask/eth-phishing-detect} - * - * @private - * @param {object} config A config object like that found at {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json} - * - */ - _setupPhishingDetector (config) { - this._phishingDetector = new PhishingDetector(config) - } -} - -module.exports = BlacklistController diff --git a/app/scripts/controllers/network/createBlockTracker.js b/app/scripts/controllers/network/createBlockTracker.js deleted file mode 100644 index 6573b18a1..000000000 --- a/app/scripts/controllers/network/createBlockTracker.js +++ /dev/null @@ -1,19 +0,0 @@ -const BlockTracker = require('eth-block-tracker') - -/** - * Creates a block tracker that sends platform events on success and failure - */ -module.exports = function createBlockTracker (args, platform) { - const blockTracker = new BlockTracker(args) - blockTracker.on('latest', () => { - if (platform && platform.sendMessage) { - platform.sendMessage({ action: 'ethereum-ping-success' }) - } - }) - blockTracker.on('error', () => { - if (platform && platform.sendMessage) { - platform.sendMessage({ action: 'ethereum-ping-error' }) - } - }) - return blockTracker -} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 884b94db3..0a6e9ecb0 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -7,14 +7,14 @@ const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const createInfuraMiddleware = require('eth-json-rpc-infura') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createInfuraClient -function createInfuraClient ({ network, platform }) { +function createInfuraClient ({ network }) { const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) const infuraProvider = providerFromMiddleware(infuraMiddleware) - const blockTracker = createBlockTracker({ provider: infuraProvider }, platform) + const blockTracker = new BlockTracker({ provider: infuraProvider }) const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware({ network }), @@ -49,6 +49,10 @@ function createNetworkAndChainIdMiddleware ({ network }) { netId = '42' chainId = '0x2a' break + case 'goerli': + netId = '5' + chainId = '0x05' + break default: throw new Error(`createInfuraClient - unknown network "${network}"`) } diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 369dcd299..a8cbf2aaf 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -5,14 +5,14 @@ const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache' const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createJsonRpcClient -function createJsonRpcClient ({ rpcUrl, platform }) { +function createJsonRpcClient ({ rpcUrl }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = createBlockTracker({ provider: blockProvider }, platform) + const blockTracker = new BlockTracker({ provider: blockProvider }) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js index 36593dc70..09b1d3c1c 100644 --- a/app/scripts/controllers/network/createLocalhostClient.js +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -3,14 +3,14 @@ const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createLocalhostClient -function createLocalhostClient ({ platform }) { +function createLocalhostClient () { const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = createBlockTracker({ provider: blockProvider, pollingInterval: 1000 }, platform) + const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js index 3190eb37c..2f7025392 100644 --- a/app/scripts/controllers/network/enums.js +++ b/app/scripts/controllers/network/enums.js @@ -3,16 +3,19 @@ const RINKEBY = 'rinkeby' const KOVAN = 'kovan' const MAINNET = 'mainnet' const LOCALHOST = 'localhost' +const GOERLI = 'goerli' const MAINNET_CODE = 1 const ROPSTEN_CODE = 3 const RINKEYBY_CODE = 4 const KOVAN_CODE = 42 +const GOERLI_CODE = 5 const ROPSTEN_DISPLAY_NAME = 'Ropsten' const RINKEBY_DISPLAY_NAME = 'Rinkeby' const KOVAN_DISPLAY_NAME = 'Kovan' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' +const GOERLI_DISPLAY_NAME = 'Goerli' module.exports = { ROPSTEN, @@ -20,12 +23,15 @@ module.exports = { KOVAN, MAINNET, LOCALHOST, + GOERLI, MAINNET_CODE, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, + GOERLI_CODE, ROPSTEN_DISPLAY_NAME, RINKEBY_DISPLAY_NAME, KOVAN_DISPLAY_NAME, MAINNET_DISPLAY_NAME, + GOERLI_DISPLAY_NAME, } diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 47432c1e2..2c68e4378 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -20,8 +20,9 @@ const { KOVAN, MAINNET, LOCALHOST, + GOERLI, } = require('./enums') -const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] +const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI] const env = process.env.METAMASK_ENV const METAMASK_DEBUG = process.env.METAMASK_DEBUG @@ -45,9 +46,8 @@ const defaultNetworkConfig = { module.exports = class NetworkController extends EventEmitter { - constructor (opts = {}, platform) { + constructor (opts = {}) { super() - this.platform = platform // parse options const providerConfig = opts.provider || defaultProviderConfig @@ -129,21 +129,22 @@ module.exports = class NetworkController extends EventEmitter { }) } - setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') { + setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs) { const providerConfig = { type: 'rpc', rpcTarget, chainId, ticker, nickname, + rpcPrefs, } this.providerConfig = providerConfig } - async setProviderType (type) { + async setProviderType (type, rpcTarget = '', ticker = 'ETH', nickname = '') { assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`) assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) - const providerConfig = { type } + const providerConfig = { type, rpcTarget, ticker, nickname } this.providerConfig = providerConfig } @@ -189,7 +190,7 @@ module.exports = class NetworkController extends EventEmitter { _configureInfuraProvider ({ type }) { log.info('NetworkController - configureInfuraProvider', type) - const networkClient = createInfuraClient({ network: type, platform: this.platform }) + const networkClient = createInfuraClient({ network: type }) this._setNetworkClient(networkClient) // setup networkConfig var settings = { @@ -200,13 +201,13 @@ module.exports = class NetworkController extends EventEmitter { _configureLocalhostProvider () { log.info('NetworkController - configureLocalhostProvider') - const networkClient = createLocalhostClient({ platform: this.platform }) + const networkClient = createLocalhostClient() this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { log.info('NetworkController - configureStandardProvider', rpcUrl) - const networkClient = createJsonRpcClient({ rpcUrl, platform: this.platform }) + const networkClient = createJsonRpcClient({ rpcUrl }) // hack to add a 'rpc' network with chainId networks.networkList['rpc'] = { chainId: chainId, diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js index 261dae721..b0afccd7e 100644 --- a/app/scripts/controllers/network/util.js +++ b/app/scripts/controllers/network/util.js @@ -3,13 +3,16 @@ const { RINKEBY, KOVAN, MAINNET, + GOERLI, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, + GOERLI_CODE, ROPSTEN_DISPLAY_NAME, RINKEBY_DISPLAY_NAME, KOVAN_DISPLAY_NAME, MAINNET_DISPLAY_NAME, + GOERLI_DISPLAY_NAME, } = require('./enums') const networkToNameMap = { @@ -17,9 +20,11 @@ const networkToNameMap = { [RINKEBY]: RINKEBY_DISPLAY_NAME, [KOVAN]: KOVAN_DISPLAY_NAME, [MAINNET]: MAINNET_DISPLAY_NAME, + [GOERLI]: GOERLI_DISPLAY_NAME, [ROPSTEN_CODE]: ROPSTEN_DISPLAY_NAME, [RINKEYBY_CODE]: RINKEBY_DISPLAY_NAME, [KOVAN_CODE]: KOVAN_DISPLAY_NAME, + [GOERLI_CODE]: GOERLI_DISPLAY_NAME, } const getNetworkDisplayName = key => networkToNameMap[key] diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 737411890..acf952bb1 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -117,6 +117,14 @@ class PreferencesController { return metaMetricsId } + getMetaMetricsId () { + return this.store.getState().metaMetricsId + } + + getParticipateInMetaMetrics () { + return this.store.getState().participateInMetaMetrics + } + setMetaMetricsSendCount (val) { this.store.updateState({ metaMetricsSendCount: val }) } @@ -331,7 +339,7 @@ class PreferencesController { } removeSuggestedTokens () { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { this.store.updateState({ suggestedTokens: {} }) resolve({}) }) @@ -388,7 +396,7 @@ class PreferencesController { const newEntry = { address, symbol, decimals } const tokens = this.store.getState().tokens const assetImages = this.getAssetImages() - const previousEntry = tokens.find((token, index) => { + const previousEntry = tokens.find((token) => { return token.address === address }) const previousIndex = tokens.indexOf(previousEntry) @@ -453,7 +461,7 @@ class PreferencesController { * */ setCurrentAccountTab (currentAccountTab) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { this.store.updateState({ currentAccountTab }) resolve() }) @@ -480,8 +488,8 @@ class PreferencesController { rpcList[index] = updatedRpc this.store.updateState({ frequentRpcListDetail: rpcList }) } else { - const { rpcUrl, chainId, ticker, nickname } = newRpcDetails - return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname) + const { rpcUrl, chainId, ticker, nickname, rpcPrefs = {} } = newRpcDetails + return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs) } return Promise.resolve(rpcList) } @@ -495,22 +503,22 @@ class PreferencesController { * @returns {Promise<array>} Promise resolving to updated frequentRpcList. * */ - addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') { - 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 + 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) } - rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname }) + if (url !== 'http://localhost:8545') { + let checkedChainId + if (!!chainId && !Number.isNaN(parseInt(chainId))) { + checkedChainId = chainId + } + rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs }) + } + this.store.updateState({ frequentRpcListDetail: rpcList }) + return Promise.resolve(rpcList) } - this.store.updateState({ frequentRpcListDetail: rpcList }) - return Promise.resolve(rpcList) - } /** * Removes custom RPC url from state. diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 2c9182b52..06c499780 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -1,9 +1,11 @@ const ObservableStore = require('obs-store') +const SafeEventEmitter = require('safe-event-emitter') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') /** * A controller that services user-approved requests for a full Ethereum provider API */ -class ProviderApprovalController { +class ProviderApprovalController extends SafeEventEmitter { /** * Determines if caching is enabled */ @@ -14,38 +16,44 @@ class ProviderApprovalController { * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) { + constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) { + super() this.approvedOrigins = {} this.closePopup = closePopup this.keyringController = keyringController this.openPopup = openPopup - this.platform = platform this.preferencesController = preferencesController - this.publicConfigStore = publicConfigStore this.store = new ObservableStore({ providerRequests: [], }) + } - if (platform && platform.addMessageListener) { - platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }, { tab }) => { - if (tab && tab.id) { - switch (action) { - case 'init-provider-request': - this._handleProviderRequest(origin, siteTitle, siteImage, force, tab.id) - break - case 'init-is-approved': - this._handleIsApproved(origin, tab.id) - break - case 'init-is-unlocked': - this._handleIsUnlocked(tab.id) - break - case 'init-privacy-request': - this._handlePrivacyRequest(tab.id) - break - } - } - }) - } + /** + * Called when a user approves access to a full Ethereum provider API + * + * @param {object} opts - opts for the middleware contains the origin for the middleware + */ + createMiddleware ({ origin, getSiteMetadata }) { + return createAsyncMiddleware(async (req, res, next) => { + // only handle requestAccounts + if (req.method !== 'eth_requestAccounts') return next() + // if already approved or privacy mode disabled, return early + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + if (this.shouldExposeAccounts(origin) && isUnlocked) { + res.result = [this.preferencesController.getSelectedAddress()] + return + } + // register the provider request + const metadata = await getSiteMetadata(origin) + this._handleProviderRequest(origin, metadata.name, metadata.icon, false, null) + // wait for resolution of request + const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) + if (approved) { + res.result = [this.preferencesController.getSelectedAddress()] + } else { + throw new Error('User denied account authorization') + } + }) } /** @@ -59,79 +67,37 @@ class ProviderApprovalController { this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { - this.approveProviderRequest(tabID) return } this.openPopup && this.openPopup() } /** - * Called by a tab to determine if an origin has been approved in the past - * - * @param {string} origin - Origin of the window - */ - _handleIsApproved (origin, tabID) { - this.platform && this.platform.sendMessage({ - action: 'answer-is-approved', - isApproved: this.approvedOrigins[origin] && this.caching, - caching: this.caching, - }, { id: tabID }) - } - - /** - * Called by a tab to determine if MetaMask is currently locked or unlocked - */ - _handleIsUnlocked (tabID) { - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { id: tabID }) - } - - /** - * Called to check privacy mode; if privacy mode is off, this will automatically enable the provider (legacy behavior) - */ - _handlePrivacyRequest (tabID) { - const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - if (!privacyMode) { - this.platform && this.platform.sendMessage({ - action: 'approve-legacy-provider-request', - selectedAddress: this.publicConfigStore.getState().selectedAddress, - }, { id: tabID }) - this.publicConfigStore.emit('update', this.publicConfigStore.getState()) - } - } - - /** * Called when a user approves access to a full Ethereum provider API * - * @param {string} tabID - ID of the target window that approved provider access + * @param {string} origin - origin of the domain that had provider access approved */ - approveProviderRequest (tabID) { + approveProviderRequestByOrigin (origin) { this.closePopup && this.closePopup() const requests = this.store.getState().providerRequests - const origin = requests.find(request => request.tabID === tabID).origin - this.platform && this.platform.sendMessage({ - action: 'approve-provider-request', - selectedAddress: this.publicConfigStore.getState().selectedAddress, - }, { id: tabID }) - this.publicConfigStore.emit('update', this.publicConfigStore.getState()) - const providerRequests = requests.filter(request => request.tabID !== tabID) + const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) this.approvedOrigins[origin] = true + this.emit(`resolvedRequest:${origin}`, { approved: true }) } /** * Called when a tab rejects access to a full Ethereum provider API * - * @param {string} tabID - ID of the target window that rejected provider access + * @param {string} origin - origin of the domain that had provider access approved */ - rejectProviderRequest (tabID) { + rejectProviderRequestByOrigin (origin) { this.closePopup && this.closePopup() const requests = this.store.getState().providerRequests - const origin = requests.find(request => request.tabID === tabID).origin - this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { id: tabID }) - const providerRequests = requests.filter(request => request.tabID !== tabID) + const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) delete this.approvedOrigins[origin] + this.emit(`resolvedRequest:${origin}`, { approved: false }) } /** @@ -149,16 +115,10 @@ class ProviderApprovalController { */ shouldExposeAccounts (origin) { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - return !privacyMode || this.approvedOrigins[origin] + const result = !privacyMode || Boolean(this.approvedOrigins[origin]) + return result } - /** - * Tells all tabs that MetaMask is now locked. This is primarily used to set - * internal flags in the contentscript and inpage script. - */ - setLocked () { - this.platform.sendMessage({ action: 'metamask-set-locked' }) - } } module.exports = ProviderApprovalController diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index 982ad2aa4..a2b5d1bae 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -8,8 +8,9 @@ const { RINKEBY, KOVAN, MAINNET, + GOERLI, } = require('./network/enums') -const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] +const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI] class RecentBlocksController { diff --git a/app/scripts/controllers/shapeshift.js b/app/scripts/controllers/shapeshift.js deleted file mode 100644 index b2a1462c2..000000000 --- a/app/scripts/controllers/shapeshift.js +++ /dev/null @@ -1,180 +0,0 @@ -const ObservableStore = require('obs-store') -const extend = require('xtend') -const log = require('loglevel') - -// every three seconds when an incomplete tx is waiting -const POLLING_INTERVAL = 3000 - -class ShapeshiftController { - - /** - * Controller responsible for managing the list of shapeshift transactions. On construction, it initiates a poll - * that queries a shapeshift.io API for updates to any pending shapeshift transactions - * - * @typedef {Object} ShapeshiftController - * @param {object} opts Overrides the defaults for the initial state of this.store - * @property {array} opts.initState initializes the the state of the ShapeshiftController. Can contain an - * shapeShiftTxList array. - * @property {array} shapeShiftTxList An array of ShapeShiftTx objects - * - */ - constructor (opts = {}) { - const initState = extend({ - shapeShiftTxList: [], - }, opts.initState) - this.store = new ObservableStore(initState) - this.pollForUpdates() - } - - /** - * Represents, and contains data about, a single shapeshift transaction. - * @typedef {Object} ShapeShiftTx - * @property {string} depositAddress - An address at which to send a crypto deposit, so that eth can be sent to the - * user's Metamask account - * @property {string} depositType - An abbreviation of the type of crypto currency to be deposited. - * @property {string} key - The 'shapeshift' key differentiates this from other types of txs in Metamask - * @property {number} time - The time at which the tx was created - * @property {object} response - Initiated as an empty object, which will be replaced by a Response object. @see {@link - * https://developer.mozilla.org/en-US/docs/Web/API/Response} - */ - - // - // PUBLIC METHODS - // - - /** - * A getter for the shapeShiftTxList property - * - * @returns {array<ShapeShiftTx>} - * - */ - getShapeShiftTxList () { - const shapeShiftTxList = this.store.getState().shapeShiftTxList - return shapeShiftTxList - } - - /** - * A getter for all ShapeShiftTx in the shapeShiftTxList that have not successfully completed a deposit. - * - * @returns {array<ShapeShiftTx>} Only includes ShapeShiftTx which has a response property with a status !== complete - * - */ - getPendingTxs () { - const txs = this.getShapeShiftTxList() - const pending = txs.filter(tx => tx.response && tx.response.status !== 'complete') - return pending - } - - /** - * A poll that exists as long as there are pending transactions. Each call attempts to update the data of any - * pendingTxs, and then calls itself again. If there are no pending txs, the recursive call is not made and - * the polling stops. - * - * this.updateTx is used to attempt the update to the pendingTxs in the ShapeShiftTxList, and that updated data - * is saved with saveTx. - * - */ - pollForUpdates () { - const pendingTxs = this.getPendingTxs() - - if (pendingTxs.length === 0) { - return - } - - Promise.all(pendingTxs.map((tx) => { - return this.updateTx(tx) - })) - .then((results) => { - results.forEach(tx => this.saveTx(tx)) - this.timeout = setTimeout(this.pollForUpdates.bind(this), POLLING_INTERVAL) - }) - } - - /** - * Attempts to update a ShapeShiftTx with data from a shapeshift.io API. Both the response and time properties - * can be updated. The response property is updated with every call, but the time property is only updated when - * the response status updates to 'complete'. This will occur once the user makes a deposit as the ShapeShiftTx - * depositAddress - * - * @param {ShapeShiftTx} tx The tx to update - * - */ - async updateTx (tx) { - try { - const url = `https://shapeshift.io/txStat/${tx.depositAddress}` - const response = await fetch(url) - const json = await response.json() - tx.response = json - if (tx.response.status === 'complete') { - tx.time = new Date().getTime() - } - return tx - } catch (err) { - log.warn(err) - } - } - - /** - * Saves an updated to a ShapeShiftTx in the shapeShiftTxList. If the passed ShapeShiftTx is not in the - * shapeShiftTxList, nothing happens. - * - * @param {ShapeShiftTx} tx The updated tx to save, if it exists in the current shapeShiftTxList - * - */ - saveTx (tx) { - const { shapeShiftTxList } = this.store.getState() - const index = shapeShiftTxList.indexOf(tx) - if (index !== -1) { - shapeShiftTxList[index] = tx - this.store.updateState({ shapeShiftTxList }) - } - } - - /** - * Removes a ShapeShiftTx from the shapeShiftTxList - * - * @param {ShapeShiftTx} tx The tx to remove - * - */ - removeShapeShiftTx (tx) { - const { shapeShiftTxList } = this.store.getState() - const index = shapeShiftTxList.indexOf(index) - if (index !== -1) { - shapeShiftTxList.splice(index, 1) - } - this.updateState({ shapeShiftTxList }) - } - - /** - * Creates a new ShapeShiftTx, adds it to the shapeShiftTxList, and initiates a new poll for updates of pending txs - * - * @param {string} depositAddress - An address at which to send a crypto deposit, so that eth can be sent to the - * user's Metamask account - * @param {string} depositType - An abbreviation of the type of crypto currency to be deposited. - * - */ - createShapeShiftTx (depositAddress, depositType) { - const state = this.store.getState() - let { shapeShiftTxList } = state - - var shapeShiftTx = { - depositAddress, - depositType, - key: 'shapeshift', - time: new Date().getTime(), - response: {}, - } - - if (!shapeShiftTxList) { - shapeShiftTxList = [shapeShiftTx] - } else { - shapeShiftTxList.push(shapeShiftTx) - } - - this.store.updateState({ shapeShiftTxList }) - this.pollForUpdates() - } - -} - -module.exports = ShapeshiftController diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 867d36433..6b6265dba 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,6 +1,8 @@ const ObservableStore = require('obs-store') const log = require('loglevel') const normalizeAddress = require('eth-sig-util').normalize +const ethUtil = require('ethereumjs-util') + // By default, poll every 3 minutes const DEFAULT_INTERVAL = 180 * 1000 @@ -28,16 +30,16 @@ class TokenRatesController { async updateExchangeRates () { if (!this.isActive) { return } const contractExchangeRates = {} - const nativeCurrency = this.currency ? this.currency.getState().nativeCurrency.toUpperCase() : 'ETH' - const pairs = this._tokens.map(token => `pairs[]=${token.address}/${nativeCurrency}`) - const query = pairs.join('&') + const nativeCurrency = this.currency ? this.currency.getState().nativeCurrency.toLowerCase() : 'eth' + const pairs = this._tokens.map(token => token.address).join(',') + const query = `contract_addresses=${pairs}&vs_currencies=${nativeCurrency}` if (this._tokens.length > 0) { try { - const response = await fetch(`https://exchanges.balanc3.net/pie?${query}&autoConversion=false`) - const { prices = [] } = await response.json() - prices.forEach(({ pair, price }) => { - const address = pair.split('/')[0] - contractExchangeRates[normalizeAddress(address)] = typeof price === 'number' ? price : 0 + const response = await fetch(`https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`) + const prices = await response.json() + this._tokens.forEach(token => { + const price = prices[token.address.toLowerCase()] || prices[ethUtil.toChecksumAddress(token.address)] + contractExchangeRates[normalizeAddress(token.address)] = price ? price[nativeCurrency] : 0 }) } catch (error) { log.warn(`MetaMask - TokenRatesController exchange rate fetch failed.`, error) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 2ce736beb..1ae925835 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,10 +3,21 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') +const abi = require('human-standard-token-abi') +const abiDecoder = require('abi-decoder') +abiDecoder.addABI(abi) +const { + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_TRANSFER_FROM, + SEND_ETHER_ACTION_KEY, + DEPLOY_CONTRACT_ACTION_KEY, + CONTRACT_INTERACTION_KEY, +} = require('../../../../ui/app/helpers/constants/transactions.js') const TransactionStateManager = require('./tx-state-manager') const TxGasUtil = require('./tx-gas-utils') const PendingTransactionTracker = require('./pending-tx-tracker') -const NonceTracker = require('./nonce-tracker') +const NonceTracker = require('nonce-tracker') const txUtils = require('./lib/util') const cleanErrorStack = require('../../lib/cleanErrorStack') const log = require('loglevel') @@ -180,9 +191,11 @@ class TransactionController extends EventEmitter { } txUtils.validateTxParams(normalizedTxParams) // construct txMeta + const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams) let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, type: TRANSACTION_TYPE_STANDARD, + transactionCategory, }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) @@ -191,7 +204,7 @@ class TransactionController extends EventEmitter { // check whether recipient account is blacklisted recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) // add default tx params - txMeta = await this.addTxGasDefaults(txMeta) + txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse) } catch (error) { log.warn(error) txMeta.loadingDefaults = false @@ -211,7 +224,7 @@ class TransactionController extends EventEmitter { @param txMeta {Object} - the txMeta object @returns {Promise<object>} resolves with txMeta */ - async addTxGasDefaults (txMeta) { + async addTxGasDefaults (txMeta, getCodeResponse) { const txParams = txMeta.txParams // ensure value txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0' @@ -222,7 +235,7 @@ class TransactionController extends EventEmitter { } txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) // set gasLimit - return await this.txGasUtil.analyzeGasUsage(txMeta) + return await this.txGasUtil.analyzeGasUsage(txMeta, getCodeResponse) } /** @@ -542,6 +555,7 @@ class TransactionController extends EventEmitter { }) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId)) + this.pendingTxTracker.on('tx:dropped', this.txStateManager.setTxStatusDropped.bind(this.txStateManager)) this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { if (!txMeta.firstRetryBlockNumber) { txMeta.firstRetryBlockNumber = latestBlockNumber @@ -556,6 +570,43 @@ class TransactionController extends EventEmitter { } /** + Returns a "type" for a transaction out of the following list: simpleSend, tokenTransfer, tokenApprove, + contractDeployment, contractMethodCall + */ + async _determineTransactionCategory (txParams) { + const { data, to } = txParams + const { name } = data && abiDecoder.decodeMethod(data) || {} + const tokenMethodName = [ + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_TRANSFER_FROM, + ].find(tokenMethodName => tokenMethodName === name && name.toLowerCase()) + + let result + let code + if (!txParams.data) { + result = SEND_ETHER_ACTION_KEY + } else if (tokenMethodName) { + result = tokenMethodName + } else if (!to) { + result = DEPLOY_CONTRACT_ACTION_KEY + } else { + try { + code = await this.query.getCode(to) + } catch (e) { + code = null + log.warn(e) + } + // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + + result = codeIsEmpty ? SEND_ETHER_ACTION_KEY : CONTRACT_INTERACTION_KEY + } + + return { transactionCategory: result, getCodeResponse: code } + } + + /** Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions in the list have the same nonce diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js deleted file mode 100644 index 421036368..000000000 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ /dev/null @@ -1,161 +0,0 @@ -const EthQuery = require('ethjs-query') -const assert = require('assert') -const Mutex = require('await-semaphore').Mutex -/** - @param opts {Object} - @param {Object} opts.provider a ethereum provider - @param {Function} opts.getPendingTransactions a function that returns an array of txMeta - whosee status is `submitted` - @param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta - whose status is `confirmed` - @class -*/ -class NonceTracker { - - constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) { - this.provider = provider - this.blockTracker = blockTracker - this.ethQuery = new EthQuery(provider) - this.getPendingTransactions = getPendingTransactions - this.getConfirmedTransactions = getConfirmedTransactions - this.lockMap = {} - } - - /** - @returns {Promise<Object>} with the key releaseLock (the gloabl mutex) - */ - async getGlobalLock () { - const globalMutex = this._lookupMutex('global') - // await global mutex free - const releaseLock = await globalMutex.acquire() - return { releaseLock } - } - - /** - * @typedef NonceDetails - * @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction. - * @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method. - * @property {number} highestSuggested - The maximum between the other two, the number returned. - */ - - /** - this will return an object with the `nextNonce` `nonceDetails` of type NonceDetails, and the releaseLock - Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding). - - @param address {string} the hex string for the address whose nonce we are calculating - @returns {Promise<NonceDetails>} - */ - async getNonceLock (address) { - // await global mutex free - await this._globalMutexFree() - // await lock free, then take lock - const releaseLock = await this._takeMutex(address) - try { - // evaluate multiple nextNonce strategies - const nonceDetails = {} - const networkNonceResult = await this._getNetworkNextNonce(address) - const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address) - const nextNetworkNonce = networkNonceResult.nonce - const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed) - - const pendingTxs = this.getPendingTransactions(address) - const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0 - - nonceDetails.params = { - highestLocallyConfirmed, - highestSuggested, - nextNetworkNonce, - } - nonceDetails.local = localNonceResult - nonceDetails.network = networkNonceResult - - const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce) - assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) - - // return nonce and release cb - return { nextNonce, nonceDetails, releaseLock } - } catch (err) { - // release lock if we encounter an error - releaseLock() - throw err - } - } - - async _globalMutexFree () { - const globalMutex = this._lookupMutex('global') - const releaseLock = await globalMutex.acquire() - releaseLock() - } - - async _takeMutex (lockId) { - const mutex = this._lookupMutex(lockId) - const releaseLock = await mutex.acquire() - return releaseLock - } - - _lookupMutex (lockId) { - let mutex = this.lockMap[lockId] - if (!mutex) { - mutex = new Mutex() - this.lockMap[lockId] = mutex - } - return mutex - } - - async _getNetworkNextNonce (address) { - // calculate next nonce - // we need to make sure our base count - // and pending count are from the same block - const blockNumber = await this.blockTracker.getLatestBlock() - const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber) - const baseCount = baseCountBN.toNumber() - assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) - const nonceDetails = { blockNumber, baseCount } - return { name: 'network', nonce: baseCount, details: nonceDetails } - } - - _getHighestLocallyConfirmed (address) { - const confirmedTransactions = this.getConfirmedTransactions(address) - const highest = this._getHighestNonce(confirmedTransactions) - return Number.isInteger(highest) ? highest + 1 : 0 - } - - _getHighestNonce (txList) { - const nonces = txList.map((txMeta) => { - const nonce = txMeta.txParams.nonce - assert(typeof nonce, 'string', 'nonces should be hex strings') - return parseInt(nonce, 16) - }) - const highestNonce = Math.max.apply(null, nonces) - return highestNonce - } - - /** - @typedef {object} highestContinuousFrom - @property {string} - name the name for how the nonce was calculated based on the data used - @property {number} - nonce the next suggested nonce - @property {object} - details the provided starting nonce that was used (for debugging) - */ - /** - @param txList {array} - list of txMeta's - @param startPoint {number} - the highest known locally confirmed nonce - @returns {highestContinuousFrom} - */ - _getHighestContinuousFrom (txList, startPoint) { - const nonces = txList.map((txMeta) => { - const nonce = txMeta.txParams.nonce - assert(typeof nonce, 'string', 'nonces should be hex strings') - return parseInt(nonce, 16) - }) - - let highest = startPoint - while (nonces.includes(highest)) { - highest++ - } - - return { name: 'local', nonce: highest, details: { startPoint, highest } } - } - -} - -module.exports = NonceTracker diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 4bf40b1db..bc11f6633 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -22,6 +22,7 @@ const EthQuery = require('ethjs-query') class PendingTransactionTracker extends EventEmitter { constructor (config) { super() + this.droppedBuffer = {} this.query = new EthQuery(config.provider) this.nonceTracker = config.nonceTracker this.getPendingTransactions = config.getPendingTransactions @@ -139,22 +140,42 @@ class PendingTransactionTracker extends EventEmitter { const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') noTxHashErr.name = 'NoTxHashError' this.emit('tx:failed', txId, noTxHashErr) + return } - // If another tx with the same nonce is mined, set as failed. + // If another tx with the same nonce is mined, set as dropped. const taken = await this._checkIfNonceIsTaken(txMeta) - if (taken) { - const nonceTakenErr = new Error('Another transaction with this nonce has been mined.') - nonceTakenErr.name = 'NonceTakenErr' - return this.emit('tx:failed', txId, nonceTakenErr) + let dropped + try { + // check the network if the nonce is ahead the tx + // and the tx has not been mined into a block + + dropped = await this._checkIftxWasDropped(txMeta) + // the dropped buffer is in case we ask a node for the tx + // that is behind the node we asked for tx count + // IS A SECURITY FOR HITTING NODES IN INFURA THAT COULD GO OUT + // OF SYNC. + // on the next block event it will return fire as dropped + if (dropped && !this.droppedBuffer[txHash]) { + this.droppedBuffer[txHash] = true + dropped = false + } else if (dropped && this.droppedBuffer[txHash]) { + // clean up + delete this.droppedBuffer[txHash] + } + + } catch (e) { + log.error(e) + } + if (taken || dropped) { + return this.emit('tx:dropped', txId) } // get latest transaction status try { - const txParams = await this.query.getTransactionByHash(txHash) - if (!txParams) return - if (txParams.blockNumber) { + const { blockNumber } = await this.query.getTransactionByHash(txHash) || {} + if (blockNumber) { this.emit('tx:confirmed', txId) } } catch (err) { @@ -165,6 +186,22 @@ 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 + @returns {boolean} + */ + + async _checkIftxWasDropped (txMeta) { + const { txParams: { nonce, from }, hash } = txMeta + const nextNonce = await this.query.getTransactionCount(from) + const { blockNumber } = await this.query.getTransactionByHash(hash) || {} + if (!blockNumber && parseInt(nextNonce) > parseInt(nonce)) { + return true + } + return false + } /** checks to see if a confirmed txMeta has the same nonce diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 765551167..287fb6f44 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -4,7 +4,9 @@ const { BnMultiplyByFraction, bnToHex, } = require('../../lib/util') +const log = require('loglevel') const { addHexPrefix } = require('ethereumjs-util') +const { SEND_ETHER_ACTION_KEY } = require('../../../../ui/app/helpers/constants/transactions.js') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys' @@ -26,12 +28,13 @@ class TxGasUtil { @param txMeta {Object} - the txMeta object @returns {object} the txMeta object with the gas written to the txParams */ - async analyzeGasUsage (txMeta) { + async analyzeGasUsage (txMeta, getCodeResponse) { const block = await this.query.getBlockByNumber('latest', false) let estimatedGasHex try { - estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) + estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit, getCodeResponse) } catch (err) { + log.warn(err) txMeta.simulationFails = { reason: err.message, errorKey: err.errorKey, @@ -54,7 +57,7 @@ class TxGasUtil { @param blockGasLimitHex {string} - hex string of the block's gas limit @returns {string} the estimated gas limit as a hex string */ - async estimateTxGas (txMeta, blockGasLimitHex) { + async estimateTxGas (txMeta, blockGasLimitHex, getCodeResponse) { const txParams = txMeta.txParams // check if gasLimit is already specified @@ -70,11 +73,10 @@ class TxGasUtil { // see if we can set the gas based on the recipient if (hasRecipient) { - const code = await this.query.getCode(recipient) // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0' - const codeIsEmpty = !code || code === '0x' || code === '0x0' + const categorizedAsSimple = txMeta.transactionCategory === SEND_ETHER_ACTION_KEY - if (codeIsEmpty) { + if (categorizedAsSimple) { // if there's data in the params, but there's no contract code, it's not a valid transaction if (txParams.data) { const err = new Error('TxGasUtil - Trying to call a function on a non-contract address') @@ -82,7 +84,7 @@ class TxGasUtil { err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY // set the response on the error so that we can see in logs what the actual response was - err.getCodeResponse = code + err.getCodeResponse = getCodeResponse throw err } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 420191d9c..1a2cb5dee 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -126,10 +126,10 @@ class TransactionStateManager extends EventEmitter { @returns {object} the txMeta */ addTx (txMeta) { - this.once(`${txMeta.id}:signed`, function (txId) { + this.once(`${txMeta.id}:signed`, function () { this.removeAllListeners(`${txMeta.id}:rejected`) }) - this.once(`${txMeta.id}:rejected`, function (txId) { + this.once(`${txMeta.id}:rejected`, function () { this.removeAllListeners(`${txMeta.id}:signed`) }) // initialize history diff --git a/app/scripts/controllers/user-actions.js b/app/scripts/controllers/user-actions.js deleted file mode 100644 index f777054b8..000000000 --- a/app/scripts/controllers/user-actions.js +++ /dev/null @@ -1,17 +0,0 @@ -const MessageManager = require('./lib/message-manager') -const PersonalMessageManager = require('./lib/personal-message-manager') -const TypedMessageManager = require('./lib/typed-message-manager') - -class UserActionController { - - constructor (opts = {}) { - - this.messageManager = new MessageManager() - this.personalMessageManager = new PersonalMessageManager() - this.typedMessageManager = new TypedMessageManager() - - } - -} - -module.exports = UserActionController diff --git a/app/scripts/createStandardProvider.js b/app/scripts/createStandardProvider.js index a5f9c5d03..2059b9b3a 100644 --- a/app/scripts/createStandardProvider.js +++ b/app/scripts/createStandardProvider.js @@ -4,18 +4,10 @@ class StandardProvider { constructor (provider) { this._provider = provider - this._onMessage('ethereumpingerror', this._onClose.bind(this)) - this._onMessage('ethereumpingsuccess', this._onConnect.bind(this)) - window.addEventListener('load', () => { - this._subscribe() - this._ping() - }) - } - - _onMessage (type, handler) { - window.addEventListener('message', function ({ data }) { - if (!data || data.type !== type) return - handler.apply(this, arguments) + this._subscribe() + // indicate that we've connected, mostly just for standard compliance + setTimeout(() => { + this._onConnect() }) } @@ -34,15 +26,6 @@ class StandardProvider { this._isConnected = true } - async _ping () { - try { - await this.send('net_version') - window.postMessage({ type: 'ethereumpingsuccess' }, '*') - } catch (error) { - window.postMessage({ type: 'ethereumpingerror' }, '*') - } - } - _subscribe () { this._provider.on('data', (error, { method, params }) => { if (!error && method === 'eth_subscription') { @@ -59,11 +42,9 @@ class StandardProvider { * @returns {Promise<*>} Promise resolving to the result if successful */ send (method, params = []) { - if (method === 'eth_requestAccounts') return this._provider.enable() - return new Promise((resolve, reject) => { try { - this._provider.sendAsync({ method, params, beta: true }, (error, response) => { + this._provider.sendAsync({ id: 1, jsonrpc: '2.0', method, params }, (error, response) => { error = error || response.error error ? reject(error) : resolve(response) }) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 68394d1ae..a4fb552f1 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -7,32 +7,12 @@ const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') const createStandardProvider = require('./createStandardProvider').default -let isEnabled = false let warned = false -let providerHandle -let isApprovedHandle -let isUnlockedHandle restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') -/** - * Adds a postMessage listener for a specific message type - * - * @param {string} messageType - postMessage type to listen for - * @param {Function} handler - event handler - * @param {boolean} remove - removes this handler after being triggered - */ -function onMessage (messageType, callback, remove) { - const handler = function ({ data }) { - if (!data || data.type !== messageType) { return } - remove && window.removeEventListener('message', handler) - callback.apply(window, arguments) - } - window.addEventListener('message', handler) -} - // // setup plugin communication // @@ -49,45 +29,16 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) -// set up a listener for when MetaMask is locked -onMessage('metamasksetlocked', () => { isEnabled = false }) - -// set up a listener for privacy mode responses -onMessage('ethereumproviderlegacy', ({ data: { selectedAddress } }) => { - isEnabled = true - setTimeout(() => { - inpageProvider.publicConfigStore.updateState({ selectedAddress }) - }, 0) -}, true) - // augment the provider with its enable method inpageProvider.enable = function ({ force } = {}) { return new Promise((resolve, reject) => { - providerHandle = ({ data: { error, selectedAddress } }) => { - if (typeof error !== 'undefined') { - reject({ - message: error, - code: 4001, - }) + inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => { + if (error) { + reject(error) } else { - window.removeEventListener('message', providerHandle) - setTimeout(() => { - inpageProvider.publicConfigStore.updateState({ selectedAddress }) - }, 0) - - // wait for the background to update with an account - inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { - if (error) { - reject(error) - } else { - isEnabled = true - resolve(response.result) - } - }) + resolve(response.result) } - } - onMessage('ethereumprovider', providerHandle, true) - window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*') + }) }) } @@ -98,31 +49,23 @@ inpageProvider.autoRefreshOnNetworkChange = true // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** - * Determines if this domain is currently enabled + * Synchronously determines if this domain is currently enabled, with a potential false negative if called to soon * - * @returns {boolean} - true if this domain is currently enabled + * @returns {boolean} - returns true if this domain is currently enabled */ isEnabled: function () { - return isEnabled + const { isEnabled } = inpageProvider.publicConfigStore.getState() + return Boolean(isEnabled) }, /** - * Determines if this domain has been previously approved + * Asynchronously determines if this domain is currently enabled * - * @returns {Promise<boolean>} - Promise resolving to true if this domain has been previously approved + * @returns {Promise<boolean>} - Promise resolving to true if this domain is currently enabled */ - isApproved: function () { - return new Promise((resolve) => { - isApprovedHandle = ({ data: { caching, isApproved } }) => { - if (caching) { - resolve(!!isApproved) - } else { - resolve(false) - } - } - onMessage('ethereumisapproved', isApprovedHandle, true) - window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*') - }) + isApproved: async function () { + const { isEnabled } = await getPublicConfigWhenReady() + return Boolean(isEnabled) }, /** @@ -130,14 +73,9 @@ inpageProvider._metamask = new Proxy({ * * @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked */ - isUnlocked: function () { - return new Promise((resolve) => { - isUnlockedHandle = ({ data: { isUnlocked } }) => { - resolve(!!isUnlocked) - } - onMessage('metamaskisunlocked', isUnlockedHandle, true) - window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*') - }) + isUnlocked: async function () { + const { isUnlocked } = await getPublicConfigWhenReady() + return Boolean(isUnlocked) }, }, { get: function (obj, prop) { @@ -149,6 +87,19 @@ 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 const proxiedInpageProvider = new Proxy(inpageProvider, { @@ -159,19 +110,6 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { window.ethereum = createStandardProvider(proxiedInpageProvider) -// detect eth_requestAccounts and pipe to enable for now -function detectAccountRequest (method) { - const originalMethod = inpageProvider[method] - inpageProvider[method] = function ({ method }) { - if (method === 'eth_requestAccounts') { - return window.ethereum.enable() - } - return originalMethod.apply(this, arguments) - } -} -detectAccountRequest('send') -detectAccountRequest('sendAsync') - // // setup web3 // @@ -218,6 +156,12 @@ inpageProvider.publicConfigStore.subscribe(function (state) { web3.eth.defaultAccount = state.selectedAddress }) +inpageProvider.publicConfigStore.subscribe(function (state) { + if (state.onboardingcomplete) { + 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... diff --git a/app/scripts/lib/backend-metametrics.js b/app/scripts/lib/backend-metametrics.js new file mode 100644 index 000000000..e3c163c1a --- /dev/null +++ b/app/scripts/lib/backend-metametrics.js @@ -0,0 +1,26 @@ +const { + getMetaMetricState, +} = require('../../../ui/app/selectors/selectors') +const { + sendMetaMetricsEvent, +} = require('../../../ui/app/helpers/utils/metametrics.util') + +const inDevelopment = process.env.NODE_ENV === 'development' + +const METAMETRICS_TRACKING_URL = inDevelopment + ? 'http://www.metamask.io/metametrics' + : 'http://www.metamask.io/metametrics-prod' + +function backEndMetaMetricsEvent (metaMaskState, eventData) { + const stateEventData = getMetaMetricState({ metamask: metaMaskState }) + + if (stateEventData.participateInMetaMetrics) { + sendMetaMetricsEvent({ + ...stateEventData, + ...eventData, + url: METAMETRICS_TRACKING_URL + '/backend', + }) + } +} + +module.exports = backEndMetaMetricsEvent diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index fbe6c6c9e..5cae83a9f 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -28,6 +28,8 @@ function getBuyEthUrl ({ network, amount, address, service }) { return 'https://www.rinkeby.io/' case 'kovan-faucet': return 'https://github.com/kovan-testnet/faucet' + case 'goerli-faucet': + return 'https://goerli-faucet.slock.it/' } throw new Error(`Unknown cryptocurrency exchange or faucet: "${service}"`) } @@ -42,6 +44,8 @@ function getDefaultServiceForNetwork (network) { return 'rinkeby-faucet' case '42': return 'kovan-faucet' + case '5': + return 'goerli-faucet' } throw new Error(`No default cryptocurrency exchange or faucet for networkId: "${network}"`) } diff --git a/app/scripts/lib/createDnodeRemoteGetter.js b/app/scripts/lib/createDnodeRemoteGetter.js new file mode 100644 index 000000000..b70d218f3 --- /dev/null +++ b/app/scripts/lib/createDnodeRemoteGetter.js @@ -0,0 +1,16 @@ +module.exports = createDnodeRemoteGetter + +function createDnodeRemoteGetter (dnode) { + let remote + + dnode.once('remote', (_remote) => { + remote = _remote + }) + + async function getRemote () { + if (remote) return remote + return await new Promise(resolve => dnode.once('remote', resolve)) + } + + return getRemote +} diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js deleted file mode 100644 index 8a939ba4e..000000000 --- a/app/scripts/lib/createProviderMiddleware.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = createProviderMiddleware - -/** - * Forwards an HTTP request to the current Web3 provider - * - * @param {{ provider: Object }} config Configuration containing current Web3 provider - */ -function createProviderMiddleware ({ provider }) { - return (req, res, next, end) => { - provider.sendAsync(req, (err, _res) => { - if (err) return end(err) - res.result = _res.result - end() - }) - } -} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index e86629590..ac41de523 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -34,7 +34,7 @@ module.exports = class MessageManager extends EventEmitter { * @property {array} messages Holds all messages that have been created by this MessageManager * */ - constructor (opts) { + constructor () { super() this.memStore = new ObservableStore({ unapprovedMsgs: {}, diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index fdb94f5ec..7c13e521a 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -36,7 +36,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { * @property {array} messages Holds all messages that have been created by this PersonalMessageManager * */ - constructor (opts) { + constructor () { super() this.memStore = new ObservableStore({ unapprovedPersonalMsgs: {}, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 4108ed4c0..242028c92 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -7,8 +7,10 @@ const EventEmitter = require('events') const pump = require('pump') const Dnode = require('dnode') +const pify = require('pify') const ObservableStore = require('obs-store') const ComposableObservableStore = require('./lib/ComposableObservableStore') +const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter') const asStream = require('obs-store/lib/asStream') const AccountTracker = require('./lib/account-tracker') const RpcEngine = require('json-rpc-engine') @@ -18,15 +20,14 @@ const createFilterMiddleware = require('eth-json-rpc-filters') const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager') const createOriginMiddleware = require('./lib/createOriginMiddleware') const createLoggerMiddleware = require('./lib/createLoggerMiddleware') -const createProviderMiddleware = require('./lib/createProviderMiddleware') +const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware') const {setupMultiplex} = require('./lib/stream-utils.js') const KeyringController = require('eth-keyring-controller') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') +const AppStateController = require('./controllers/app-state') const CurrencyController = require('./controllers/currency') -const ShapeShiftController = require('./controllers/shapeshift') const InfuraController = require('./controllers/infura') -const BlacklistController = require('./controllers/blacklist') const CachedBalancesController = require('./controllers/cached-balances') const RecentBlocksController = require('./controllers/recent-blocks') const MessageManager = require('./lib/message-manager') @@ -53,7 +54,12 @@ const HW_WALLETS_KEYRINGS = [TrezorKeyring.type, LedgerBridgeKeyring.type] const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const sigUtil = require('eth-sig-util') -const { AddressBookController } = require('gaba') +const { + AddressBookController, + ShapeShiftController, + PhishingController, +} = require('gaba') +const backEndMetaMetricsEvent = require('./lib/backend-metametrics') module.exports = class MetamaskController extends EventEmitter { @@ -86,7 +92,7 @@ module.exports = class MetamaskController extends EventEmitter { this.createVaultMutex = new Mutex() // network store - this.networkController = new NetworkController(initState.NetworkController, this.platform) + this.networkController = new NetworkController(initState.NetworkController) // preferences controller this.preferencesController = new PreferencesController({ @@ -96,6 +102,12 @@ module.exports = class MetamaskController extends EventEmitter { network: this.networkController, }) + // app-state controller + this.appStateController = new AppStateController({ + preferencesStore: this.preferencesController.store, + onInactiveTimeout: () => this.setLocked(), + }) + // currency controller this.currencyController = new CurrencyController({ initState: initState.CurrencyController, @@ -109,8 +121,7 @@ module.exports = class MetamaskController extends EventEmitter { }) this.infuraController.scheduleInfuraNetworkCheck() - this.blacklistController = new BlacklistController() - this.blacklistController.scheduleUpdates() + this.phishingController = new PhishingController() // rpc provider this.initializeProvider() @@ -190,10 +201,26 @@ module.exports = class MetamaskController extends EventEmitter { }) this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx()) - this.txController.on(`tx:status-update`, (txId, status) => { + this.txController.on(`tx:status-update`, async (txId, status) => { if (status === 'confirmed' || status === 'failed') { const txMeta = this.txController.txStateManager.getTx(txId) this.platform.showTransactionNotification(txMeta) + + const { txReceipt } = txMeta + const participateInMetaMetrics = this.preferencesController.getParticipateInMetaMetrics() + if (txReceipt && txReceipt.status === '0x0' && participateInMetaMetrics) { + const metamaskState = await this.getState() + backEndMetaMetricsEvent(metamaskState, { + customVariables: { + errorMessage: txMeta.simulationFails.reason, + }, + eventOpts: { + category: 'backend', + action: 'Transactions', + name: 'On Chain Failure', + }, + }) + } } }) @@ -210,38 +237,40 @@ module.exports = class MetamaskController extends EventEmitter { }) this.balancesController.updateAllBalances() - this.shapeshiftController = new ShapeShiftController({ - initState: initState.ShapeShiftController, - }) + this.shapeshiftController = new ShapeShiftController(undefined, initState.ShapeShiftController) this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) - this.publicConfigStore = this.initPublicConfigStore() + + // ensure isClientOpenAndUnlocked is updated when memState updates + this.on('update', (memState) => { + this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen + }) this.providerApprovalController = new ProviderApprovalController({ closePopup: opts.closePopup, keyringController: this.keyringController, openPopup: opts.openPopup, - platform: opts.platform, preferencesController: this.preferencesController, - publicConfigStore: this.publicConfigStore, }) this.store.updateStructure({ + AppStateController: this.appStateController.store, TransactionController: this.txController.store, KeyringController: this.keyringController.store, PreferencesController: this.preferencesController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyController.store, - ShapeShiftController: this.shapeshiftController.store, + ShapeShiftController: this.shapeshiftController, NetworkController: this.networkController.store, InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, }) this.memStore = new ComposableObservableStore(null, { + AppStateController: this.appStateController.store, NetworkController: this.networkController.store, AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, @@ -256,7 +285,7 @@ module.exports = class MetamaskController extends EventEmitter { RecentBlocksController: this.recentBlocksController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyController.store, - ShapeshiftController: this.shapeshiftController.store, + ShapeshiftController: this.shapeshiftController, InfuraController: this.infuraController.store, ProviderApprovalController: this.providerApprovalController.store, }) @@ -305,21 +334,32 @@ module.exports = class MetamaskController extends EventEmitter { * Constructor helper: initialize a public config store. * This store is used to make some config info available to Dapps synchronously. */ - initPublicConfigStore () { - // get init state + createPublicConfigStore ({ checkIsEnabled }) { + // subset of state for metamask inpage provider const publicConfigStore = new ObservableStore() - // memStore -> transform -> publicConfigStore - this.on('update', (memState) => { - this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen + // setup memStore subscription hooks + this.on('update', updatePublicConfigStore) + updatePublicConfigStore(this.getState()) + + publicConfigStore.destroy = () => { + this.removeEventListener && this.removeEventListener('update', updatePublicConfigStore) + } + + function updatePublicConfigStore (memState) { const publicState = selectPublicState(memState) publicConfigStore.putState(publicState) - }) + } - function selectPublicState (memState) { + function selectPublicState ({ isUnlocked, selectedAddress, network, completedOnboarding }) { + const isEnabled = checkIsEnabled() + const isReady = isUnlocked && isEnabled const result = { - selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined, - networkVersion: memState.network, + isUnlocked, + isEnabled, + selectedAddress: isReady ? selectedAddress : undefined, + networkVersion: network, + onboardingcomplete: completedOnboarding, } return result } @@ -429,6 +469,9 @@ module.exports = class MetamaskController extends EventEmitter { // AddressController setAddressBook: this.addressBookController.set.bind(this.addressBookController), + // AppStateController + setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), + // KeyringController setLocked: nodeify(this.setLocked, this), createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), @@ -459,9 +502,10 @@ module.exports = class MetamaskController extends EventEmitter { signTypedMessage: nodeify(this.signTypedMessage, this), cancelTypedMessage: this.cancelTypedMessage.bind(this), - approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController), + // provider approval + approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), + rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), - rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController), } } @@ -1189,9 +1233,8 @@ module.exports = class MetamaskController extends EventEmitter { * with higher gas. * * @param {string} txId - The ID of the transaction to speed up. - * @param {Function} cb - The callback function called with a full state update. */ - async retryTransaction (txId, gasPrice, cb) { + async retryTransaction (txId, gasPrice) { await this.txController.retryTransaction(txId, gasPrice) const state = await this.getState() return state @@ -1204,7 +1247,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string=} customGasPrice - the hex value to use for the cancel transaction * @returns {object} MetaMask state */ - async createCancelTransaction (originalTxId, customGasPrice, cb) { + async createCancelTransaction (originalTxId, customGasPrice) { try { await this.txController.createCancelTransaction(originalTxId, customGasPrice) const state = await this.getState() @@ -1214,7 +1257,7 @@ module.exports = class MetamaskController extends EventEmitter { } } - async createSpeedUpTransaction (originalTxId, customGasPrice, cb) { + async createSpeedUpTransaction (originalTxId, customGasPrice) { await this.txController.createSpeedUpTransaction(originalTxId, customGasPrice) const state = await this.getState() return state @@ -1269,7 +1312,7 @@ module.exports = class MetamaskController extends EventEmitter { */ setupUntrustedCommunication (connectionStream, originDomain) { // Check if new connection is blacklisted - if (this.blacklistController.checkForPhishing(originDomain)) { + if (this.phishingController.test(originDomain)) { log.debug('MetaMask - sending phishing warning for', originDomain) this.sendPhishingWarning(connectionStream, originDomain) return @@ -1278,8 +1321,9 @@ module.exports = class MetamaskController extends EventEmitter { // setup multiplexing const mux = setupMultiplex(connectionStream) // connect features - this.setupProviderConnection(mux.createStream('provider'), originDomain) - this.setupPublicConfig(mux.createStream('publicConfig')) + const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain) + this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi) + this.setupPublicConfig(mux.createStream('publicConfig'), originDomain) } /** @@ -1352,7 +1396,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {*} outStream - The stream to provide over. * @param {string} origin - The URI of the requesting resource. */ - setupProviderConnection (outStream, origin) { + setupProviderConnection (outStream, origin, publicApi) { // setup json rpc engine stack const engine = new RpcEngine() const provider = this.provider @@ -1372,8 +1416,13 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(subscriptionManager.middleware) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) + // requestAccounts + engine.push(this.providerApprovalController.createMiddleware({ + origin, + getSiteMetadata: publicApi && publicApi.getSiteMetadata, + })) // forward to metamask primary provider - engine.push(createProviderMiddleware({ provider })) + engine.push(providerAsMiddleware(provider)) // setup connection const providerStream = createEngineStream({ engine }) @@ -1400,12 +1449,18 @@ module.exports = class MetamaskController extends EventEmitter { * * @param {*} outStream - The stream to provide public config over. */ - setupPublicConfig (outStream) { - const configStream = asStream(this.publicConfigStore) + setupPublicConfig (outStream, originDomain) { + const configStore = this.createPublicConfigStore({ + // check the providerApprovalController's approvedOrigins + checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(originDomain), + }) + const configStream = asStream(configStore) + pump( configStream, outStream, (err) => { + configStore.destroy() configStream.destroy() if (err) log.error(err) } @@ -1413,6 +1468,38 @@ module.exports = class MetamaskController extends EventEmitter { } /** + * A method for providing our public api over a stream. + * This includes a method for setting site metadata like title and image + * + * @param {*} outStream - The stream to provide the api over. + */ + setupPublicApi (outStream) { + const dnode = Dnode() + // connect dnode api to remote connection + pump( + outStream, + dnode, + outStream, + (err) => { + // report any error + if (err) log.error(err) + } + ) + + const getRemote = createDnodeRemoteGetter(dnode) + + const publicApi = { + // wrap with an await remote + getSiteMetadata: async () => { + const remote = await getRemote() + return await pify(remote.getSiteMetadata)() + }, + } + + return publicApi + } + + /** * Handle a KeyringController update * @param {object} state the KC state * @return {Promise<void>} @@ -1544,7 +1631,7 @@ module.exports = class MetamaskController extends EventEmitter { * @property {string} depositType - An abbreviation of the type of crypto currency to be deposited. */ createShapeShiftTx (depositAddress, depositType) { - this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) + this.shapeshiftController.createTransaction(depositAddress, depositType) } // network @@ -1557,9 +1644,9 @@ module.exports = class MetamaskController extends EventEmitter { * @returns {Promise<String>} - The RPC Target URL confirmed. */ - async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname) { - await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname }) - this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname) + async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname, rpcPrefs) { + await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname, rpcPrefs }) + this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname, rpcPrefs) return rpcUrl } @@ -1572,15 +1659,15 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} nickname - Optional nickname of the selected network. * @returns {Promise<String>} - The RPC Target URL confirmed. */ - async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') { + async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail() const rpcSettings = frequentRpcListDetail.find((rpc) => rpcTarget === rpc.rpcUrl) if (rpcSettings) { - this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname) + this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname, rpcPrefs) } else { - this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname) - await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname) + this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname, rpcPrefs) + await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname, rpcPrefs) } return rpcTarget } @@ -1705,18 +1792,17 @@ module.exports = class MetamaskController extends EventEmitter { */ /** - * Adds a domain to the {@link BlacklistController} whitelist + * Adds a domain to the PhishingController whitelist * @param {string} hostname the domain to whitelist */ whitelistPhishingDomain (hostname) { - return this.blacklistController.whitelistDomain(hostname) + return this.phishingController.bypass(hostname) } /** * Locks MetaMask */ setLocked () { - this.providerApprovalController.setLocked() return this.keyringController.setLocked() } } diff --git a/app/scripts/migrations/024.js b/app/scripts/migrations/024.js index d0b276a79..6239bab13 100644 --- a/app/scripts/migrations/024.js +++ b/app/scripts/migrations/024.js @@ -27,7 +27,7 @@ function transformState (state) { const newState = state if (!newState.TransactionController) return newState const transactions = newState.TransactionController.transactions - newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => { + newState.TransactionController.transactions = transactions.map((txMeta, _) => { if ( txMeta.status === 'unapproved' && txMeta.txParams && diff --git a/app/scripts/migrations/025.js b/app/scripts/migrations/025.js index fc3b20a44..fd4faa782 100644 --- a/app/scripts/migrations/025.js +++ b/app/scripts/migrations/025.js @@ -43,7 +43,7 @@ function normalizeTxParams (txParams) { // functions that handle normalizing of that key in txParams const whiteList = { from: from => ethUtil.addHexPrefix(from).toLowerCase(), - to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(), + to: () => ethUtil.addHexPrefix(txParams.to).toLowerCase(), nonce: nonce => ethUtil.addHexPrefix(nonce), value: value => ethUtil.addHexPrefix(value), data: data => ethUtil.addHexPrefix(data), diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 099b0d7ea..0c2d222b8 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -60,20 +60,6 @@ class ExtensionPlatform { } } - addMessageListener (cb) { - extension.runtime.onMessage.addListener(cb) - } - - sendMessage (message, query = {}) { - const id = query.id - delete query.id - extension.tabs.query({ ...query }, tabs => { - tabs.forEach(tab => { - extension.tabs.sendMessage(id || tab.id, message) - }) - }) - } - _showConfirmedTransaction (txMeta) { this._subscribeToNotificationClicked() |