diff options
author | Thomas <tmashuang@gmail.com> | 2018-04-24 02:22:24 +0800 |
---|---|---|
committer | Thomas <tmashuang@gmail.com> | 2018-04-24 02:22:24 +0800 |
commit | 010092312998e90688e1bfb004eb55c67a337429 (patch) | |
tree | 93a174b2eb7074283d6e43bcc98bc1f2c7925d57 /app/scripts | |
parent | ddece0cc11215b323df96287f39e6758aa559f77 (diff) | |
parent | 74c419f9b65841c2966d1fe3b7414bf11e09e0d8 (diff) | |
download | tangerine-wallet-browser-010092312998e90688e1bfb004eb55c67a337429.tar.gz tangerine-wallet-browser-010092312998e90688e1bfb004eb55c67a337429.tar.zst tangerine-wallet-browser-010092312998e90688e1bfb004eb55c67a337429.zip |
Merge branch 'master' into testing
Diffstat (limited to 'app/scripts')
44 files changed, 1133 insertions, 272 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index 5878cd2e8..6550e8944 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -21,12 +21,16 @@ const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const EdgeEncryptor = require('./edge-encryptor') const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code') const getObjStructure = require('./lib/getObjStructure') +const { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_FULLSCREEN, +} = require('./lib/enums') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = process.env.METAMASK_DEBUG -window.log = log -log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') +log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') const platform = new ExtensionPlatform() const notificationManager = new NotificationManager() @@ -44,7 +48,7 @@ const isEdge = !isIE && !!window.StyleMedia let popupIsOpen = false let notificationIsOpen = false -let openMetamaskTabsIDs = {} +const openMetamaskTabsIDs = {} // state persistence const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) @@ -173,7 +177,7 @@ function setupController (initState, initLangCode) { return versionedData } - function persistData(state) { + function persistData (state) { if (!state) { throw new Error('MetaMask - updated state is missing', state) } @@ -192,30 +196,53 @@ function setupController (initState, initLangCode) { // // connect to other contexts // - extension.runtime.onConnect.addListener(connectRemote) + + const metamaskInternalProcessHash = { + [ENVIRONMENT_TYPE_POPUP]: true, + [ENVIRONMENT_TYPE_NOTIFICATION]: true, + [ENVIRONMENT_TYPE_FULLSCREEN]: true, + } + + const isClientOpenStatus = () => { + return popupIsOpen || Boolean(Object.keys(openMetamaskTabsIDs).length) || notificationIsOpen + } + function connectRemote (remotePort) { - const isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' + const processName = remotePort.name + const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName] const portStream = new PortStream(remotePort) + if (isMetaMaskInternalProcess) { // communication with popup - popupIsOpen = popupIsOpen || (remotePort.name === 'popup') + controller.isClientOpen = true controller.setupTrustedCommunication(portStream, 'MetaMask') - // record popup as closed - if (remotePort.sender.url.match(/home.html$/)) { - openMetamaskTabsIDs[remotePort.sender.tab.id] = true - } - if (remotePort.name === 'popup') { + + if (processName === ENVIRONMENT_TYPE_POPUP) { + popupIsOpen = true + endOfStream(portStream, () => { popupIsOpen = false - if (remotePort.sender.url.match(/home.html$/)) { - openMetamaskTabsIDs[remotePort.sender.tab.id] = false - } + controller.isClientOpen = isClientOpenStatus() }) } - if (remotePort.name === 'notification') { + + if (processName === ENVIRONMENT_TYPE_NOTIFICATION) { + notificationIsOpen = true + endOfStream(portStream, () => { notificationIsOpen = false + controller.isClientOpen = isClientOpenStatus() + }) + } + + if (processName === ENVIRONMENT_TYPE_FULLSCREEN) { + const tabId = remotePort.sender.tab.id + openMetamaskTabsIDs[tabId] = true + + endOfStream(portStream, () => { + delete openMetamaskTabsIDs[tabId] + controller.isClientOpen = isClientOpenStatus() }) } } else { @@ -258,10 +285,11 @@ function setupController (initState, initLangCode) { // popup trigger function triggerUi () { - extension.tabs.query({ active: true }, (tabs) => { - const currentlyActiveMetamaskTab = tabs.find(tab => openMetamaskTabsIDs[tab.id]) - if (!popupIsOpen && !currentlyActiveMetamaskTab && !notificationIsOpen) notificationManager.showPopup() - notificationIsOpen = true + extension.tabs.query({ active: true }, tabs => { + const currentlyActiveMetamaskTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id])) + if (!popupIsOpen && !currentlyActiveMetamaskTab && !notificationIsOpen) { + notificationManager.showPopup() + } }) } diff --git a/app/scripts/config.js b/app/scripts/config.js index a8470ed82..e6f70ca2b 100644 --- a/app/scripts/config.js +++ b/app/scripts/config.js @@ -15,7 +15,41 @@ const BETA_UI_NETWORK_TYPE = 'networkBeta' global.METAMASK_DEBUG = process.env.METAMASK_DEBUG -module.exports = { +/** + * @typedef {Object} UrlConfig + * @property {string} localhost URL of local RPC provider + * @property {string} mainnet URL of mainnet RPC provider + * @property {string} ropsten URL of Ropsten testnet RPC provider + * @property {string} kovan URL of Kovan testnet RPC provider + * @property {string} rinkeby URL of Rinkeby testnet RPC provider + */ + +/** + * @typedef {Object} NameConfig + * @property {string} 3 URL of local RPC provider + * @property {string} 4 URL of mainnet RPC provider + * @property {string} 42 URL of Ropsten testnet RPC provider + */ + +/** + * @typedef {Object} EnumConfig + * @property {string} DEFAULT_RPC Default network provider URL + * @property {string} OLD_UI_NETWORK_TYPE Network associated with old UI + * @property {string} BETA_UI_NETWORK_TYPE Network associated with new UI + */ + +/** + * @typedef {Object} Config + * @property {UrlConfig} network Network configuration parameters + * @property {UrlConfig} networkBeta Beta UI network configuration parameters + * @property {NameConfig} networkNames Network name configuration parameters + * @property {EnumConfig} enums Application-wide string constants + */ + +/** + * @type {Config} + **/ +const config = { network: { localhost: LOCALHOST_RPC_URL, mainnet: MAINET_RPC_URL, @@ -23,7 +57,6 @@ module.exports = { kovan: KOVAN_RPC_URL, rinkeby: RINKEBY_RPC_URL, }, - // Used for beta UI networkBeta: { localhost: LOCALHOST_RPC_URL, mainnet: MAINET_RPC_URL_BETA, @@ -42,3 +75,5 @@ module.exports = { BETA_UI_NETWORK_TYPE, }, } + +module.exports = config diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index fe1766273..dbf1c6d4c 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -23,6 +23,9 @@ if (shouldInjectWeb3()) { setupStreams() } +/** + * Creates a script tag that injects inpage.js + */ function setupInjection () { try { // inject in-page script @@ -37,6 +40,10 @@ function setupInjection () { } } +/** + * Sets up two-way communication streams between the + * browser extension and local per-page browser context + */ function setupStreams () { // setup communication to page and plugin const pageStream = new LocalMessageDuplexStream({ @@ -89,17 +96,34 @@ function setupStreams () { mux.ignoreStream('publicConfig') } + +/** + * Error handler for page to plugin stream disconnections + * + * @param {string} remoteLabel Remote stream name + * @param {Error} err Stream connection error + */ function logStreamDisconnectWarning (remoteLabel, err) { let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}` if (err) warningMsg += '\n' + err.stack console.warn(warningMsg) } +/** + * Determines if Web3 should be injected + * + * @returns {boolean} {@code true} if Web3 should be injected + */ function shouldInjectWeb3 () { return doctypeCheck() && suffixCheck() && documentElementCheck() && !blacklistedDomainCheck() } +/** + * Checks the doctype of the current document if it exists + * + * @returns {boolean} {@code true} if the doctype is html or if none exists + */ function doctypeCheck () { const doctype = window.document.doctype if (doctype) { @@ -109,6 +133,11 @@ function doctypeCheck () { } } +/** + * Checks the current document extension + * + * @returns {boolean} {@code true} if the current extension is not prohibited + */ function suffixCheck () { var prohibitedTypes = ['xml', 'pdf'] var currentUrl = window.location.href @@ -122,6 +151,11 @@ function suffixCheck () { return true } +/** + * Checks the documentElement of the current document + * + * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + */ function documentElementCheck () { var documentElement = document.documentElement.nodeName if (documentElement) { @@ -130,6 +164,11 @@ function documentElementCheck () { return true } +/** + * Checks if the current domain is blacklisted + * + * @returns {boolean} {@code true} if the current domain is blacklisted + */ function blacklistedDomainCheck () { var blacklistedDomains = [ 'uscourts.gov', @@ -148,6 +187,9 @@ function blacklistedDomainCheck () { return false } +/** + * Redirects the current page to a phishing information page + */ function redirectToPhishingWarning () { console.log('MetaMask - redirecting to phishing warning') window.location.href = 'https://metamask.io/phishing.html' diff --git a/app/scripts/controllers/address-book.js b/app/scripts/controllers/address-book.js index 6fb4ee114..c91e6b2e4 100644 --- a/app/scripts/controllers/address-book.js +++ b/app/scripts/controllers/address-book.js @@ -4,9 +4,22 @@ const extend = require('xtend') class AddressBookController { - // Controller in charge of managing the address book functionality from the - // recipients field on the send screen. Manages a history of all saved - // addresses and all currently owned addresses. + /** + * Controller in charge of managing the address book functionality from the + * recipients field on the send screen. Manages a history of all saved + * addresses and all currently owned addresses. + * + * @typedef {Object} AddressBookController + * @param {object} opts Overrides the defaults for the initial state of this.store + * @property {array} opts.initState initializes the the state of the AddressBookController. Can contain an + * addressBook property to initialize the addressBook array + * @param {KeyringController} keyringController (Soon to be deprecated) The keyringController used in the current + * MetamaskController. Contains the identities used in this AddressBookController. + * @property {object} store The the store of the current users address book + * @property {array} store.addressBook An array of addresses and nicknames. These are set by the user when sending + * to a new address. + * + */ constructor (opts = {}, keyringController) { const initState = extend({ addressBook: [], @@ -19,7 +32,14 @@ class AddressBookController { // PUBLIC METHODS // - // Sets a new address book in store by accepting a new address and nickname. + /** + * Sets a new address book in store by accepting a new address and nickname. + * + * @param {string} address A hex address of a new account that the user is sending to. + * @param {string} name The name the user wishes to associate with the new account + * @returns {Promise<void>} Promise resolves with undefined + * + */ setAddressBook (address, name) { return this._addToAddressBook(address, name) .then((addressBook) => { @@ -30,14 +50,16 @@ class AddressBookController { }) } - // - // PRIVATE METHODS - // - - - // Performs the logic to add the address and name into the address book. The - // pushed object is an object of two fields. Current behavior does not set an - // upper limit to the number of addresses. + /** + * Performs the logic to add the address and name into the address book. The pushed object is an object of two + * fields. Current behavior does not set an upper limit to the number of addresses. + * + * @private + * @param {string} address A hex address of a new account that the user is sending to. + * @param {string} name The name the user wishes to associate with the new account + * @returns {Promise<array>} Promises the updated addressBook array + * + */ _addToAddressBook (address, name) { const addressBook = this._getAddressBook() const identities = this._getIdentities() @@ -62,14 +84,26 @@ class AddressBookController { return Promise.resolve(addressBook) } - // Internal method to get the address book. Current persistence behavior - // should not require that this method be called from the UI directly. + /** + * Internal method to get the address book. Current persistence behavior should not require that this method be + * called from the UI directly. + * + * @private + * @returns {array} The addressBook array from the store. + * + */ _getAddressBook () { return this.store.getState().addressBook } - // Retrieves identities from the keyring controller in order to avoid - // duplication + /** + * Retrieves identities from the keyring controller in order to avoid + * duplication + * + * @deprecated + * @returns {array} Returns the identies array from the keyringContoller's state + * + */ _getIdentities () { return this.keyringController.memStore.getState().identities } diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index df41c90c0..d965f80b8 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -1,6 +1,7 @@ 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') diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js index 907b087cf..1a6802f9a 100644 --- a/app/scripts/controllers/computed-balances.js +++ b/app/scripts/controllers/computed-balances.js @@ -2,8 +2,24 @@ const ObservableStore = require('obs-store') const extend = require('xtend') const BalanceController = require('./balance') -class ComputedbalancesController { +/** + * @typedef {Object} ComputedBalancesOptions + * @property {Object} accountTracker Account tracker store reference + * @property {Object} txController Token controller reference + * @property {Object} blockTracker Block tracker reference + * @property {Object} initState Initial state to populate this internal store with + */ +/** + * Background controller responsible for syncing + * and computing ETH balances for all accounts + */ +class ComputedbalancesController { + /** + * Creates a new controller instance + * + * @param {ComputedBalancesOptions} [opts] Controller configuration parameters + */ constructor (opts = {}) { const { accountTracker, txController, blockTracker } = opts this.accountTracker = accountTracker @@ -19,6 +35,9 @@ class ComputedbalancesController { this._initBalanceUpdating() } + /** + * Updates balances associated with each internal address + */ updateAllBalances () { Object.keys(this.balances).forEach((balance) => { const address = balance.address @@ -26,12 +45,23 @@ class ComputedbalancesController { }) } + /** + * Initializes internal address tracking + * + * @private + */ _initBalanceUpdating () { const store = this.accountTracker.store.getState() this.syncAllAccountsFromStore(store) this.accountTracker.store.subscribe(this.syncAllAccountsFromStore.bind(this)) } + /** + * Uses current account state to sync and track all + * addresses associated with the current account + * + * @param {{ accounts: Object }} store Account tracking state + */ syncAllAccountsFromStore (store) { const upstream = Object.keys(store.accounts) const balances = Object.keys(this.balances) @@ -50,6 +80,13 @@ class ComputedbalancesController { }) } + /** + * Conditionally establishes a new subscription + * to track an address associated with the current + * account + * + * @param {string} address Address to conditionally subscribe to + */ trackAddressIfNotAlready (address) { const state = this.store.getState() if (!(address in state.computedBalances)) { @@ -57,6 +94,12 @@ class ComputedbalancesController { } } + /** + * Establishes a new subscription to track an + * address associated with the current account + * + * @param {string} address Address to conditionally subscribe to + */ trackAddress (address) { const updater = new BalanceController({ address, diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index 36b8808aa..480c08b1c 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -1,11 +1,28 @@ -const ObservableStore = require('obs-store') + const ObservableStore = require('obs-store') const extend = require('xtend') +const log = require('loglevel') // every ten minutes const POLLING_INTERVAL = 600000 class CurrencyController { + /** + * Controller responsible for managing data associated with the currently selected currency. + * + * @typedef {Object} CurrencyController + * @param {object} opts Overrides the defaults for the initial state of this.store + * @property {array} opts.initState initializes the the state of the CurrencyController. Can contain an + * currentCurrency, conversionRate and conversionDate properties + * @property {string} currentCurrency A 2-4 character shorthand that describes a specific currency, currently + * selected by the user + * @property {number} conversionRate The conversion rate from ETH to the selected currency. + * @property {string} conversionDate The date at which the conversion rate was set. Expressed in in milliseconds + * since midnight of January 1, 1970 + * @property {number} conversionInterval The id of the interval created by the scheduleConversionInterval method. + * Used to clear an existing interval on subsequent calls of that method. + * + */ constructor (opts = {}) { const initState = extend({ currentCurrency: 'usd', @@ -19,30 +36,73 @@ class CurrencyController { // PUBLIC METHODS // + /** + * A getter for the currentCurrency property + * + * @returns {string} A 2-4 character shorthand that describes a specific currency, currently selected by the user + * + */ getCurrentCurrency () { return this.store.getState().currentCurrency } + /** + * A setter for the currentCurrency property + * + * @param {string} currentCurrency The new currency to set as the currentCurrency in the store + * + */ setCurrentCurrency (currentCurrency) { this.store.updateState({ currentCurrency }) } + /** + * A getter for the conversionRate property + * + * @returns {string} The conversion rate from ETH to the selected currency. + * + */ getConversionRate () { return this.store.getState().conversionRate } + /** + * A setter for the conversionRate property + * + * @param {number} conversionRate The new rate to set as the conversionRate in the store + * + */ setConversionRate (conversionRate) { this.store.updateState({ conversionRate }) } + /** + * A getter for the conversionDate property + * + * @returns {string} The date at which the conversion rate was set. Expressed in milliseconds since midnight of + * January 1, 1970 + * + */ getConversionDate () { return this.store.getState().conversionDate } + /** + * A setter for the conversionDate property + * + * @param {number} conversionDate The date, expressed in milliseconds since midnight of January 1, 1970, that the + * conversionRate was set + * + */ setConversionDate (conversionDate) { this.store.updateState({ conversionDate }) } + /** + * Updates the conversionRate and conversionDate properties associated with the currentCurrency. Updated info is + * fetched from an external API + * + */ async updateConversionRate () { let currentCurrency try { @@ -58,6 +118,12 @@ class CurrencyController { } } + /** + * Creates a new poll, using setInterval, to periodically call updateConversionRate. The id of the interval is + * stored at the controller's conversionInterval property. If it is called and such an id already exists, the + * previous interval is clear and a new one is created. + * + */ scheduleConversionInterval () { if (this.conversionInterval) { clearInterval(this.conversionInterval) diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index c6b4c9de2..8f6dd837e 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const extend = require('xtend') +const log = require('loglevel') // every ten minutes const POLLING_INTERVAL = 10 * 60 * 1000 diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 617456cd7..45574e673 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -9,6 +9,7 @@ const extend = require('xtend') const EthQuery = require('eth-query') const createEventEmitterProxy = require('../lib/events-proxy.js') const networkConfig = require('../config.js') +const log = require('loglevel') const { OLD_UI_NETWORK_TYPE, DEFAULT_RPC } = networkConfig.enums const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet'] diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index b4819d951..d4d508026 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -4,6 +4,21 @@ const extend = require('xtend') class PreferencesController { + /** + * + * @typedef {Object} PreferencesController + * @param {object} opts Overrides the defaults for the initial state of this.store + * @property {object} store The an object containing a users preferences, stored in local storage + * @property {array} store.frequentRpcList A list of custom rpcs to provide the user + * @property {string} store.currentAccountTab Indicates the selected tab in the ui + * @property {array} store.tokens The tokens the user wants display in their token lists + * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI + * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the + * user wishes to see that feature + * @property {string} store.currentLocale The preferred language locale key + * @property {string} store.selectedAddress A hex string that matches the currently selected address in the app + * + */ constructor (opts = {}) { const initState = extend({ frequentRpcList: [], @@ -17,18 +32,43 @@ class PreferencesController { } // PUBLIC METHODS + /** + * Setter for the `useBlockie` property + * + * @param {boolean} val Whether or not the user prefers blockie indicators + * + */ setUseBlockie (val) { this.store.updateState({ useBlockie: val }) } + /** + * Getter for the `useBlockie` property + * + * @returns {boolean} this.store.useBlockie + * + */ getUseBlockie () { return this.store.getState().useBlockie } + /** + * Setter for the `currentLocale` property + * + * @param {string} key he preferred language locale key + * + */ setCurrentLocale (key) { this.store.updateState({ currentLocale: key }) } + /** + * Setter for the `selectedAddress` property + * + * @param {string} _address A new hex address for an account + * @returns {Promise<void>} Promise resolves with undefined + * + */ setSelectedAddress (_address) { return new Promise((resolve, reject) => { const address = normalizeAddress(_address) @@ -37,10 +77,37 @@ class PreferencesController { }) } + /** + * Getter for the `selectedAddress` property + * + * @returns {string} The hex address for the currently selected account + * + */ getSelectedAddress () { return this.store.getState().selectedAddress } + /** + * Contains data about tokens users add to their account. + * @typedef {Object} AddedToken + * @property {string} address - The hex address for the token contract. Will be all lower cased and hex-prefixed. + * @property {string} symbol - The symbol of the token, usually 3 or 4 capitalized letters + * {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#symbol} + * @property {boolean} decimals - The number of decimals the token uses. + * {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#decimals} + */ + + /** + * Adds a new token to the token array, or updates the token if passed an address that already exists. + * Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects. + * @see AddedToken {@link AddedToken} + * + * @param {string} rawAddress Hex address of the token contract. May or may not be a checksum address. + * @param {string} symbol The symbol of the token + * @param {number} decimals The number of decimals the token uses. + * @returns {Promise<array>} Promises the new array of AddedToken objects. + * + */ async addToken (rawAddress, symbol, decimals) { const address = normalizeAddress(rawAddress) const newEntry = { address, symbol, decimals } @@ -62,6 +129,13 @@ class PreferencesController { return Promise.resolve(tokens) } + /** + * Removes a specified token from the tokens array. + * + * @param {string} rawAddress Hex address of the token contract to remove. + * @returns {Promise<array>} The new array of AddedToken objects + * + */ removeToken (rawAddress) { const tokens = this.store.getState().tokens @@ -71,10 +145,23 @@ class PreferencesController { return Promise.resolve(updatedTokens) } + /** + * A getter for the `tokens` property + * + * @returns {array} The current array of AddedToken objects + * + */ getTokens () { return this.store.getState().tokens } + /** + * Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list. + * + * @param {string} _url The the new rpc url to add to the updated list + * @returns {Promise<void>} Promise resolves with undefined + * + */ updateFrequentRpcList (_url) { return this.addToFrequentRpcList(_url) .then((rpcList) => { @@ -83,6 +170,13 @@ class PreferencesController { }) } + /** + * Setter for the `currentAccountTab` property + * + * @param {string} currentAccountTab Specifies the new tab to be marked as current + * @returns {Promise<void>} Promise resolves with undefined + * + */ setCurrentAccountTab (currentAccountTab) { return new Promise((resolve, reject) => { this.store.updateState({ currentAccountTab }) @@ -90,6 +184,15 @@ class PreferencesController { }) } + /** + * Returns an updated rpcList based on the passed url and the current list. + * The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the + * end of the list. The current list is modified and returned as a promise. + * + * @param {string} _url The rpc url to add to the frequentRpcList. + * @returns {Promise<array>} The updated frequentRpcList. + * + */ addToFrequentRpcList (_url) { const rpcList = this.getFrequentRpcList() const index = rpcList.findIndex((element) => { return element === _url }) @@ -105,10 +208,24 @@ class PreferencesController { return Promise.resolve(rpcList) } + /** + * Getter for the `frequentRpcList` property. + * + * @returns {array<string>} An array of one or two rpc urls. + * + */ getFrequentRpcList () { return this.store.getState().frequentRpcList } + /** + * Updates the `featureFlags` property, which is an object. One property within that object will be set to a boolean. + * + * @param {string} feature A key that corresponds to a UI feature. + * @param {boolean} activated Indicates whether or not the UI feature should be displayed + * @returns {Promise<object>} Promises a new object; the updated featureFlags object. + * + */ setFeatureFlag (feature, activated) { const currentFeatureFlags = this.store.getState().featureFlags const updatedFeatureFlags = { @@ -121,6 +238,13 @@ class PreferencesController { return Promise.resolve(updatedFeatureFlags) } + /** + * A getter for the `featureFlags` property + * + * @returns {object} A key-boolean map, where keys refer to features and booleans to whether the + * user wishes to see that feature + * + */ getFeatureFlags () { return this.store.getState().featureFlags } diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index 4ae3810eb..0c1ee4e38 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -2,6 +2,7 @@ const ObservableStore = require('obs-store') const extend = require('xtend') const BN = require('ethereumjs-util').BN const EthQuery = require('eth-query') +const log = require('loglevel') class RecentBlocksController { diff --git a/app/scripts/controllers/shapeshift.js b/app/scripts/controllers/shapeshift.js index 3bbfaa1c5..b2a1462c2 100644 --- a/app/scripts/controllers/shapeshift.js +++ b/app/scripts/controllers/shapeshift.js @@ -1,11 +1,23 @@ 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: [], @@ -14,21 +26,54 @@ class ShapeshiftController { 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() @@ -45,6 +90,15 @@ class ShapeshiftController { }) } + /** + * 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}` @@ -60,6 +114,13 @@ class ShapeshiftController { } } + /** + * 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) @@ -69,6 +130,12 @@ class ShapeshiftController { } } + /** + * 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) @@ -78,6 +145,14 @@ class ShapeshiftController { 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 diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js new file mode 100644 index 000000000..28409ea10 --- /dev/null +++ b/app/scripts/controllers/token-rates.js @@ -0,0 +1,77 @@ +const ObservableStore = require('obs-store') + +// By default, poll every 3 minutes +const DEFAULT_INTERVAL = 180 * 1000 + +/** + * A controller that polls for token exchange + * rates based on a user's current token list + */ +class TokenRatesController { + /** + * Creates a TokenRatesController + * + * @param {Object} [config] - Options to configure controller + */ + constructor ({ interval = DEFAULT_INTERVAL, preferences } = {}) { + this.store = new ObservableStore() + this.preferences = preferences + this.interval = interval + } + + /** + * Updates exchange rates for all tokens + */ + async updateExchangeRates () { + if (!this.isActive) { return } + const contractExchangeRates = {} + for (const i in this._tokens) { + const address = this._tokens[i].address + contractExchangeRates[address] = await this.fetchExchangeRate(address) + } + this.store.putState({ contractExchangeRates }) + } + + /** + * Fetches a token exchange rate by address + * + * @param {String} address - Token contract address + */ + async fetchExchangeRate (address) { + try { + const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`) + const json = await response.json() + return json && json.length ? json[0].averagePrice : 0 + } catch (error) { } + } + + /** + * @type {Number} + */ + set interval (interval) { + this._handle && clearInterval(this._handle) + if (!interval) { return } + this._handle = setInterval(() => { this.updateExchangeRates() }, interval) + } + + /** + * @type {Object} + */ + set preferences (preferences) { + this._preferences && this._preferences.unsubscribe() + if (!preferences) { return } + this._preferences = preferences + this.tokens = preferences.getState().tokens + preferences.subscribe(({ tokens = [] }) => { this.tokens = tokens }) + } + + /** + * @type {Array} + */ + set tokens (tokens) { + this._tokens = tokens + this.updateExchangeRates() + } +} + +module.exports = TokenRatesController diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 336b0d8f7..c8211ebd7 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -7,6 +7,7 @@ const TransactionStateManager = require('../lib/tx-state-manager') const TxGasUtil = require('../lib/tx-gas-utils') const PendingTransactionTracker = require('../lib/pending-tx-tracker') const NonceTracker = require('../lib/nonce-tracker') +const log = require('loglevel') /* Transaction Controller is an aggregate of sub-controllers and trackers diff --git a/app/scripts/edge-encryptor.js b/app/scripts/edge-encryptor.js index 24c0c93a8..dcb06873b 100644 --- a/app/scripts/edge-encryptor.js +++ b/app/scripts/edge-encryptor.js @@ -1,69 +1,97 @@ const asmcrypto = require('asmcrypto.js') const Unibabel = require('browserify-unibabel') +/** + * A Microsoft Edge-specific encryption class that exposes + * the interface expected by eth-keykeyring-controller + */ class EdgeEncryptor { + /** + * Encrypts an arbitrary object to ciphertext + * + * @param {string} password Used to generate a key to encrypt the data + * @param {Object} dataObject Data to encrypt + * @returns {Promise<string>} Promise resolving to an object with ciphertext + */ + encrypt (password, dataObject) { + var salt = this._generateSalt() + return this._keyFromPassword(password, salt) + .then(function (key) { + var data = JSON.stringify(dataObject) + var dataBuffer = Unibabel.utf8ToBuffer(data) + var vector = global.crypto.getRandomValues(new Uint8Array(16)) + var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector) - encrypt (password, dataObject) { + var buffer = new Uint8Array(resultbuffer) + var vectorStr = Unibabel.bufferToBase64(vector) + var vaultStr = Unibabel.bufferToBase64(buffer) + return JSON.stringify({ + data: vaultStr, + iv: vectorStr, + salt: salt, + }) + }) + } - var salt = this._generateSalt() - return this._keyFromPassword(password, salt) - .then(function (key) { + /** + * Decrypts an arbitrary object from ciphertext + * + * @param {string} password Used to generate a key to decrypt the data + * @param {string} text Ciphertext of an encrypted object + * @returns {Promise<Object>} Promise resolving to copy of decrypted object + */ + decrypt (password, text) { + const payload = JSON.parse(text) + const salt = payload.salt + return this._keyFromPassword(password, salt) + .then(function (key) { + const encryptedData = Unibabel.base64ToBuffer(payload.data) + const vector = Unibabel.base64ToBuffer(payload.iv) + return new Promise((resolve, reject) => { + var result + try { + result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector) + } catch (err) { + return reject(new Error('Incorrect password')) + } + const decryptedData = new Uint8Array(result) + const decryptedStr = Unibabel.bufferToUtf8(decryptedData) + const decryptedObj = JSON.parse(decryptedStr) + resolve(decryptedObj) + }) + }) + } - var data = JSON.stringify(dataObject) - var dataBuffer = Unibabel.utf8ToBuffer(data) - var vector = global.crypto.getRandomValues(new Uint8Array(16)) - var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector) + /** + * Retrieves a cryptographic key using a password + * + * @private + * @param {string} password Password used to unlock a cryptographic key + * @param {string} salt Random base64 data + * @returns {Promise<Object>} Promise resolving to a derived key + */ + _keyFromPassword (password, salt) { - var buffer = new Uint8Array(resultbuffer) - var vectorStr = Unibabel.bufferToBase64(vector) - var vaultStr = Unibabel.bufferToBase64(buffer) - return JSON.stringify({ - data: vaultStr, - iv: vectorStr, - salt: salt, - }) - }) - } + var passBuffer = Unibabel.utf8ToBuffer(password) + var saltBuffer = Unibabel.base64ToBuffer(salt) + return new Promise((resolve) => { + var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000) + resolve(key) + }) + } - decrypt (password, text) { - - const payload = JSON.parse(text) - const salt = payload.salt - return this._keyFromPassword(password, salt) - .then(function (key) { - const encryptedData = Unibabel.base64ToBuffer(payload.data) - const vector = Unibabel.base64ToBuffer(payload.iv) - return new Promise((resolve, reject) => { - var result - try { - result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector) - } catch (err) { - return reject(new Error('Incorrect password')) - } - const decryptedData = new Uint8Array(result) - const decryptedStr = Unibabel.bufferToUtf8(decryptedData) - const decryptedObj = JSON.parse(decryptedStr) - resolve(decryptedObj) - }) - }) - } - - _keyFromPassword (password, salt) { - - var passBuffer = Unibabel.utf8ToBuffer(password) - var saltBuffer = Unibabel.base64ToBuffer(salt) - return new Promise((resolve) => { - var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000) - resolve(key) - }) - } - - _generateSalt (byteCount = 32) { - var view = new Uint8Array(byteCount) - global.crypto.getRandomValues(view) - var b64encoded = btoa(String.fromCharCode.apply(null, view)) - return b64encoded - } + /** + * Generates random base64 encoded data + * + * @private + * @returns {string} Randomized base64 encoded data + */ + _generateSalt (byteCount = 32) { + var view = new Uint8Array(byteCount) + global.crypto.getRandomValues(view) + var b64encoded = btoa(String.fromCharCode.apply(null, view)) + return b64encoded + } } module.exports = EdgeEncryptor diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 3063df627..144534f43 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -2,10 +2,16 @@ const env = process.env.METAMASK_ENV const METAMASK_DEBUG = process.env.METAMASK_DEBUG -// -// The default state of MetaMask -// -module.exports = { +/** + * @typedef {Object} FirstTimeState + * @property {Object} config Initial configuration parameters + * @property {Object} NetworkController Network controller state + */ + +/** + * @type {FirstTimeState} + */ +const initialState = { config: {}, NetworkController: { provider: { @@ -13,3 +19,5 @@ module.exports = { }, }, } + +module.exports = initialState diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ec99bfc35..6d16eebd4 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -3,16 +3,11 @@ cleanContextForImports() require('web3/dist/web3.min.js') const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') -// const PingStream = require('ping-pong-stream/ping') -// const endOfStream = require('end-of-stream') const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js') restoreContextAfterImports() -const METAMASK_DEBUG = process.env.METAMASK_DEBUG -window.log = log -log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') - +log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') // // setup plugin communication @@ -47,20 +42,20 @@ log.debug('MetaMask - injected web3') setupDappAutoReload(web3, inpageProvider.publicConfigStore) // set web3 defaultAccount - inpageProvider.publicConfigStore.subscribe(function (state) { web3.eth.defaultAccount = state.selectedAddress }) -// -// util -// - // 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... var __define +/** + * Caches reference to global define object and deletes it to + * avoid conflicts with other global define objects, such as + * AMD's define function + */ function cleanContextForImports () { __define = global.define try { @@ -70,6 +65,9 @@ function cleanContextForImports () { } } +/** + * Restores global define object from cached reference + */ function restoreContextAfterImports () { try { global.define = __define diff --git a/app/scripts/lib/ComposableObservableStore.js b/app/scripts/lib/ComposableObservableStore.js new file mode 100644 index 000000000..d5ee708a1 --- /dev/null +++ b/app/scripts/lib/ComposableObservableStore.js @@ -0,0 +1,49 @@ +const ObservableStore = require('obs-store') + +/** + * An ObservableStore that can composes a flat + * structure of child stores based on configuration + */ +class ComposableObservableStore extends ObservableStore { + /** + * Create a new store + * + * @param {Object} [initState] - The initial store state + * @param {Object} [config] - Map of internal state keys to child stores + */ + constructor (initState, config) { + super(initState) + this.updateStructure(config) + } + + /** + * Composes a new internal store subscription structure + * + * @param {Object} [config] - Map of internal state keys to child stores + */ + updateStructure (config) { + this.config = config + this.removeAllListeners() + for (const key in config) { + config[key].subscribe((state) => { + this.updateState({ [key]: state }) + }) + } + } + + /** + * Merges all child store state into a single object rather than + * returning an object keyed by child store class name + * + * @returns {Object} - Object containing merged child store state + */ + getFlatState () { + let flatState = {} + for (const key in this.config) { + flatState = { ...flatState, ...this.config[key].getState() } + } + return flatState + } +} + +module.exports = ComposableObservableStore diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index b9dde3c28..4e2d0bc79 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -1,5 +1,16 @@ module.exports = getBuyEthUrl +/** + * Gives the caller a url at which the user can acquire eth, depending on the network they are in + * + * @param {object} opts Options required to determine the correct url + * @param {string} opts.network The network for which to return a url + * @param {string} opts.amount The amount of ETH to buy on coinbase. Only relevant if network === '1'. + * @param {string} opts.address The address the bought ETH should be sent to. Only relevant if network === '1'. + * @returns {string|undefined} The url at which the user can access ETH, while in the given network. If the passed + * network does not match any of the specified cases, or if no network is given, returns undefined. + * + */ function getBuyEthUrl ({ network, amount, address }) { let url switch (network) { diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 34b603b96..63d27c40e 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -102,7 +102,6 @@ ConfigManager.prototype.setShowSeedWords = function (should) { this.setData(data) } - ConfigManager.prototype.getShouldShowSeedWords = function () { var data = this.getData() return data.showSeedWords @@ -118,6 +117,27 @@ ConfigManager.prototype.getSeedWords = function () { var data = this.getData() return data.seedWords } + +/** + * Called to set the isRevealingSeedWords flag. This happens only when the user chooses to reveal + * the seed words and not during the first time flow. + * @param {boolean} reveal - Value to set the isRevealingSeedWords flag. + */ +ConfigManager.prototype.setIsRevealingSeedWords = function (reveal = false) { + const data = this.getData() + data.isRevealingSeedWords = reveal + this.setData(data) +} + +/** + * Returns the isRevealingSeedWords flag. + * @returns {boolean|undefined} + */ +ConfigManager.prototype.getIsRevealingSeedWords = function () { + const data = this.getData() + return data.isRevealingSeedWords +} + ConfigManager.prototype.setRpcTarget = function (rpcUrl) { var config = this.getConfig() config.provider = { diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js index 2707cbd9e..996c3477c 100644 --- a/app/scripts/lib/createLoggerMiddleware.js +++ b/app/scripts/lib/createLoggerMiddleware.js @@ -1,14 +1,20 @@ -// log rpc activity +const log = require('loglevel') + module.exports = createLoggerMiddleware -function createLoggerMiddleware ({ origin }) { - return function loggerMiddleware (req, res, next, end) { - next((cb) => { +/** + * Returns a middleware that logs RPC activity + * @param {{ origin: string }} opts - The middleware options + * @returns {Function} + */ +function createLoggerMiddleware (opts) { + return function loggerMiddleware (/** @type {any} */ req, /** @type {any} */ res, /** @type {Function} */ next) { + next((/** @type {Function} */ cb) => { if (res.error) { log.error('Error in RPC response:\n', res) } if (req.isMetamaskInternal) return - log.info(`RPC (${origin}):`, req, '->', res) + log.info(`RPC (${opts.origin}):`, req, '->', res) cb() }) } diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js index f8bdb2dc2..98bb0e3b3 100644 --- a/app/scripts/lib/createOriginMiddleware.js +++ b/app/scripts/lib/createOriginMiddleware.js @@ -1,9 +1,13 @@ -// append dapp origin domain to request module.exports = createOriginMiddleware -function createOriginMiddleware ({ origin }) { - return function originMiddleware (req, res, next, end) { - req.origin = origin +/** + * Returns a middleware that appends the DApp origin to request + * @param {{ origin: string }} opts - The middleware options + * @returns {Function} + */ +function createOriginMiddleware (opts) { + return function originMiddleware (/** @type {any} */ req, /** @type {any} */ _, /** @type {Function} */ next) { + req.origin = opts.origin next() } } diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js index 4e667bac2..8a939ba4e 100644 --- a/app/scripts/lib/createProviderMiddleware.js +++ b/app/scripts/lib/createProviderMiddleware.js @@ -1,6 +1,10 @@ module.exports = createProviderMiddleware -// forward requests to provider +/** + * 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) => { diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js new file mode 100644 index 000000000..0a3afca47 --- /dev/null +++ b/app/scripts/lib/enums.js @@ -0,0 +1,9 @@ +const ENVIRONMENT_TYPE_POPUP = 'popup' +const ENVIRONMENT_TYPE_NOTIFICATION = 'notification' +const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen' + +module.exports = { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_FULLSCREEN, +} diff --git a/app/scripts/lib/environment-type.js b/app/scripts/lib/environment-type.js deleted file mode 100644 index 7966926eb..000000000 --- a/app/scripts/lib/environment-type.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function environmentType () { - const url = window.location.href - if (url.match(/popup.html$/)) { - return 'popup' - } else if (url.match(/home.html$/)) { - return 'responsive' - } else { - return 'notification' - } -} diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js index c0a490b05..f83773ccc 100644 --- a/app/scripts/lib/events-proxy.js +++ b/app/scripts/lib/events-proxy.js @@ -1,26 +1,37 @@ +/** + * Returns an EventEmitter that proxies events from the given event emitter + * @param {any} eventEmitter + * @param {object} listeners - The listeners to proxy to + * @returns {any} + */ module.exports = function createEventEmitterProxy (eventEmitter, listeners) { let target = eventEmitter const eventHandlers = listeners || {} - const proxy = new Proxy({}, { - get: (obj, name) => { + const proxy = /** @type {any} */ (new Proxy({}, { + get: (_, name) => { // intercept listeners if (name === 'on') return addListener if (name === 'setTarget') return setTarget if (name === 'proxyEventHandlers') return eventHandlers - return target[name] + return (/** @type {any} */ (target))[name] }, - set: (obj, name, value) => { + set: (_, name, value) => { target[name] = value return true }, - }) - function setTarget (eventEmitter) { + })) + function setTarget (/** @type {EventEmitter} */ eventEmitter) { target = eventEmitter // migrate listeners Object.keys(eventHandlers).forEach((name) => { - eventHandlers[name].forEach((handler) => target.on(name, handler)) + /** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler)) }) } + /** + * Attaches a function to be called whenever the specified event is emitted + * @param {string} name + * @param {Function} handler + */ function addListener (name, handler) { if (!eventHandlers[name]) eventHandlers[name] = [] eventHandlers[name].push(handler) diff --git a/app/scripts/lib/get-first-preferred-lang-code.js b/app/scripts/lib/get-first-preferred-lang-code.js index e3635434e..5473fccf0 100644 --- a/app/scripts/lib/get-first-preferred-lang-code.js +++ b/app/scripts/lib/get-first-preferred-lang-code.js @@ -4,6 +4,13 @@ const allLocales = require('../../_locales/index.json') const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-')) +/** + * Returns a preferred language code, based on settings within the user's browser. If we have no translations for the + * users preferred locales, 'en' is returned. + * + * @returns {Promise<string>} Promises a locale code, either one from the user's preferred list that we have a translation for, or 'en' + * + */ async function getFirstPreferredLangCode () { const userPreferredLocaleCodes = await promisify( extension.i18n.getAcceptLanguages, diff --git a/app/scripts/lib/hex-to-bn.js b/app/scripts/lib/hex-to-bn.js index 184217279..b28746920 100644 --- a/app/scripts/lib/hex-to-bn.js +++ b/app/scripts/lib/hex-to-bn.js @@ -1,6 +1,11 @@ -const ethUtil = require('ethereumjs-util') +const ethUtil = (/** @type {object} */ (require('ethereumjs-util'))) const BN = ethUtil.BN +/** + * Returns a [BinaryNumber]{@link BN} representation of the given hex value + * @param {string} hex + * @return {any} + */ module.exports = function hexToBn (hex) { return new BN(ethUtil.stripHexPrefix(hex), 16) } diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js deleted file mode 100644 index ad3e825c0..000000000 --- a/app/scripts/lib/is-popup-or-notification.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = function isPopupOrNotification () { - const url = window.location.href - // if (url.match(/popup.html$/) || url.match(/home.html$/)) { - // Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js) - // Revert below regexes to above commented out regexes before merge to master - if (url.match(/popup.html(?:\?.+)*$/) || - url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) { - return 'popup' - } else { - return 'notification' - } -} diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js index 5b47985f6..139ff86bd 100644 --- a/app/scripts/lib/local-store.js +++ b/app/scripts/lib/local-store.js @@ -1,10 +1,13 @@ -// We should not rely on local storage in an extension! -// We should use this instead! -// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/local - const extension = require('extensionizer') +const log = require('loglevel') +/** + * A wrapper around the extension's storage local API + */ module.exports = class ExtensionStore { + /** + * @constructor + */ constructor() { this.isSupported = !!(extension.storage.local) if (!this.isSupported) { @@ -12,6 +15,10 @@ module.exports = class ExtensionStore { } } + /** + * Returns all of the keys currently saved + * @return {Promise<*>} + */ async get() { if (!this.isSupported) return undefined const result = await this._get() @@ -24,14 +31,24 @@ module.exports = class ExtensionStore { } } + /** + * Sets the key in local state + * @param {object} state - The state to set + * @return {Promise<void>} + */ async set(state) { return this._set(state) } + /** + * Returns all of the keys currently saved + * @private + * @return {object} the key-value map from local storage + */ _get() { const local = extension.storage.local return new Promise((resolve, reject) => { - local.get(null, (result) => { + local.get(null, (/** @type {any} */ result) => { const err = extension.runtime.lastError if (err) { reject(err) @@ -42,6 +59,12 @@ module.exports = class ExtensionStore { }) } + /** + * Sets the key in local state + * @param {object} obj - The key to set + * @return {Promise<void>} + * @private + */ _set(obj) { const local = extension.storage.local return new Promise((resolve, reject) => { @@ -57,6 +80,11 @@ module.exports = class ExtensionStore { } } +/** + * Returns whether or not the given object contains no keys + * @param {object} obj - The object to check + * @returns {boolean} + */ function isEmpty(obj) { return Object.keys(obj).length === 0 } diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index 85c2717ea..345ca8001 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -1,7 +1,23 @@ const EventEmitter = require('events') +/** + * @typedef {object} Migration + * @property {number} version - The migration version + * @property {Function} migrate - Returns a promise of the migrated data + */ + +/** + * @typedef {object} MigratorOptions + * @property {Array<Migration>} [migrations] - The list of migrations to apply + * @property {number} [defaultVersion] - The version to use in the initial state + */ + class Migrator extends EventEmitter { + /** + * @constructor + * @param {MigratorOptions} opts + */ constructor (opts = {}) { super() const migrations = opts.migrations || [] @@ -42,19 +58,30 @@ class Migrator extends EventEmitter { return versionedData - // migration is "pending" if it has a higher - // version number than currentVersion + /** + * Returns whether or not the migration is pending + * + * A migration is considered "pending" if it has a higher + * version number than the current version. + * @param {Migration} migration + * @returns {boolean} + */ function migrationIsPending (migration) { return migration.version > versionedData.meta.version } } - generateInitialState (initState) { + /** + * Returns the initial state for the migrator + * @param {object} [data] - The data for the initial state + * @returns {{meta: {version: number}, data: any}} + */ + generateInitialState (data) { return { meta: { version: this.defaultVersion, }, - data: initState, + data, } } diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 9b595d93c..25be6537b 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,6 +1,14 @@ const promiseToCallback = require('promise-to-callback') const noop = function () {} +/** + * A generator that returns a function which, when passed a promise, can treat that promise as a node style callback. + * The prime advantage being that callbacks are better for error handling. + * + * @param {Function} fn The function to handle as a callback + * @param {Object} context The context in which the fn is to be called, most often a this reference + * + */ module.exports = function nodeify (fn, context) { return function () { const args = [].slice.call(arguments) diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 6602f5aa8..43a7d0b42 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -3,6 +3,7 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const createId = require('./random-id') const hexRe = /^[0-9A-Fa-f]+$/g +const log = require('loglevel') module.exports = class PersonalMessageManager extends EventEmitter { diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js index a9716fb00..5c4224fd9 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -6,6 +6,13 @@ module.exports = PortDuplexStream inherits(PortDuplexStream, Duplex) +/** + * Creates a stream that's both readable and writable. + * The stream supports arbitrary objects. + * + * @class + * @param {Object} port Remote Port object + */ function PortDuplexStream (port) { Duplex.call(this, { objectMode: true, @@ -15,8 +22,13 @@ function PortDuplexStream (port) { port.onDisconnect.addListener(this._onDisconnect.bind(this)) } -// private - +/** + * Callback triggered when a message is received from + * the remote Port associated with this Stream. + * + * @private + * @param {Object} msg - Payload from the onMessage listener of Port + */ PortDuplexStream.prototype._onMessage = function (msg) { if (Buffer.isBuffer(msg)) { delete msg._isBuffer @@ -27,14 +39,31 @@ PortDuplexStream.prototype._onMessage = function (msg) { } } +/** + * Callback triggered when the remote Port + * associated with this Stream disconnects. + * + * @private + */ PortDuplexStream.prototype._onDisconnect = function () { this.destroy() } -// stream plumbing - +/** + * Explicitly sets read operations to a no-op + */ PortDuplexStream.prototype._read = noop + +/** + * Called internally when data should be written to + * this writable stream. + * + * @private + * @param {*} msg Arbitrary object to write + * @param {string} encoding Encoding to use when writing payload + * @param {Function} cb Called when writing is complete or an error occurs + */ PortDuplexStream.prototype._write = function (msg, encoding, cb) { try { if (Buffer.isBuffer(msg)) { diff --git a/app/scripts/lib/seed-phrase-verifier.js b/app/scripts/lib/seed-phrase-verifier.js index 9cea22029..7ba712c0d 100644 --- a/app/scripts/lib/seed-phrase-verifier.js +++ b/app/scripts/lib/seed-phrase-verifier.js @@ -1,4 +1,5 @@ const KeyringController = require('eth-keyring-controller') +const log = require('loglevel') const seedPhraseVerifier = { diff --git a/app/scripts/lib/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js index 40343f017..02690a948 100644 --- a/app/scripts/lib/setupMetamaskMeshMetrics.js +++ b/app/scripts/lib/setupMetamaskMeshMetrics.js @@ -1,6 +1,9 @@ module.exports = setupMetamaskMeshMetrics +/** + * Injects an iframe into the current document for testing + */ function setupMetamaskMeshMetrics() { const testingContainer = document.createElement('iframe') testingContainer.src = 'https://metamask.github.io/mesh-testing/' diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js index 8bb0b4f3c..3dbc064b5 100644 --- a/app/scripts/lib/stream-utils.js +++ b/app/scripts/lib/stream-utils.js @@ -8,20 +8,34 @@ module.exports = { setupMultiplex: setupMultiplex, } +/** + * Returns a stream transform that parses JSON strings passing through + * @return {stream.Transform} + */ function jsonParseStream () { - return Through.obj(function (serialized, encoding, cb) { + return Through.obj(function (serialized, _, cb) { this.push(JSON.parse(serialized)) cb() }) } +/** + * Returns a stream transform that calls {@code JSON.stringify} + * on objects passing through + * @return {stream.Transform} the stream transform + */ function jsonStringifyStream () { - return Through.obj(function (obj, encoding, cb) { + return Through.obj(function (obj, _, cb) { this.push(JSON.stringify(obj)) cb() }) } +/** + * Sets up stream multiplexing for the given stream + * @param {any} connectionStream - the stream to mux + * @return {stream.Stream} the multiplexed stream + */ function setupMultiplex (connectionStream) { const mux = new ObjectMultiplex() pump( diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index 8b760790e..60042155e 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -3,7 +3,7 @@ const ObservableStore = require('obs-store') const createId = require('./random-id') const assert = require('assert') const sigUtil = require('eth-sig-util') - +const log = require('loglevel') module.exports = class TypedMessageManager extends EventEmitter { constructor (opts) { diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 6dee9edf0..431d1e59c 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -1,20 +1,53 @@ const ethUtil = require('ethereumjs-util') const assert = require('assert') const BN = require('bn.js') +const { + ENVIRONMENT_TYPE_POPUP, + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_FULLSCREEN, +} = require('./enums') -module.exports = { - getStack, - sufficientBalance, - hexToBn, - bnToHex, - BnMultiplyByFraction, -} - +/** + * Generates an example stack trace + * + * @returns {string} A stack trace + * + */ function getStack () { const stack = new Error('Stack trace generator - not an error').stack return stack } +/** + * Used to determine the window type through which the app is being viewed. + * - 'popup' refers to the extension opened through the browser app icon (in top right corner in chrome and firefox) + * - 'responsive' refers to the main browser window + * - 'notification' refers to the popup that appears in its own window when taking action outside of metamask + * + * @returns {string} A single word label that represents the type of window through which the app is being viewed + * + */ +const getEnvironmentType = (url = window.location.href) => { + if (url.match(/popup.html(?:\?.+)*$/)) { + return ENVIRONMENT_TYPE_POPUP + } else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) { + return ENVIRONMENT_TYPE_FULLSCREEN + } else { + return ENVIRONMENT_TYPE_NOTIFICATION + } +} + +/** + * Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee + * + * @param {object} txParams Contains data about a transaction + * @param {string} txParams.gas The gas for a transaction + * @param {string} txParams.gasPrice The price per gas for the transaction + * @param {string} txParams.value The value of ETH to send + * @param {string} hexBalance A balance of ETH represented as a hex string + * @returns {boolean} Whether the balance is greater than or equal to the value plus the value of gas times gasPrice + * + */ function sufficientBalance (txParams, hexBalance) { // validate hexBalance is a hex string assert.equal(typeof hexBalance, 'string', 'sufficientBalance - hexBalance is not a hex string') @@ -29,16 +62,48 @@ function sufficientBalance (txParams, hexBalance) { return balance.gte(maxCost) } +/** + * Converts a BN object to a hex string with a '0x' prefix + * + * @param {BN} inputBn The BN to convert to a hex string + * @returns {string} A '0x' prefixed hex string + * + */ function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) } +/** + * Converts a hex string to a BN object + * + * @param {string} inputHex A number represented as a hex string + * @returns {Object} A BN object + * + */ function hexToBn (inputHex) { return new BN(ethUtil.stripHexPrefix(inputHex), 16) } +/** + * Used to multiply a BN by a fraction + * + * @param {BN} targetBN The number to multiply by a fraction + * @param {number|string} numerator The numerator of the fraction multiplier + * @param {number|string} denominator The denominator of the fraction multiplier + * @returns {BN} The product of the multiplication + * + */ function BnMultiplyByFraction (targetBN, numerator, denominator) { const numBN = new BN(numerator) const denomBN = new BN(denominator) return targetBN.mul(numBN).div(denomBN) } + +module.exports = { + getStack, + getEnvironmentType, + sufficientBalance, + hexToBn, + bnToHex, + BnMultiplyByFraction, +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b96acc9da..782bc50ac 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -5,10 +5,10 @@ */ const EventEmitter = require('events') -const extend = require('xtend') const pump = require('pump') const Dnode = require('dnode') const ObservableStore = require('obs-store') +const ComposableObservableStore = require('./lib/ComposableObservableStore') const asStream = require('obs-store/lib/asStream') const AccountTracker = require('./lib/account-tracker') const RpcEngine = require('json-rpc-engine') @@ -34,6 +34,7 @@ const PersonalMessageManager = require('./lib/personal-message-manager') const TypedMessageManager = require('./lib/typed-message-manager') const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') +const TokenRatesController = require('./controllers/token-rates') const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') @@ -44,6 +45,7 @@ const BN = require('ethereumjs-util').BN const GWEI_BN = new BN('1000000000') const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') +const log = require('loglevel') module.exports = class MetamaskController extends EventEmitter { @@ -65,7 +67,7 @@ module.exports = class MetamaskController extends EventEmitter { this.platform = opts.platform // observable state store - this.store = new ObservableStore(initState) + this.store = new ComposableObservableStore(initState) // lock to ensure only one vault created at once this.createVaultMutex = new Mutex() @@ -104,6 +106,11 @@ module.exports = class MetamaskController extends EventEmitter { this.provider = this.initializeProvider() this.blockTracker = this.provider._blockTracker + // token exchange rate tracker + this.tokenRatesController = new TokenRatesController({ + preferences: this.preferencesController.store, + }) + this.recentBlocksController = new RecentBlocksController({ blockTracker: this.blockTracker, provider: this.provider, @@ -184,53 +191,37 @@ module.exports = class MetamaskController extends EventEmitter { this.typedMessageManager = new TypedMessageManager() this.publicConfigStore = this.initPublicConfigStore() - // manual disk state subscriptions - this.txController.store.subscribe((state) => { - this.store.updateState({ TransactionController: state }) - }) - this.keyringController.store.subscribe((state) => { - this.store.updateState({ KeyringController: state }) - }) - this.preferencesController.store.subscribe((state) => { - this.store.updateState({ PreferencesController: state }) - }) - this.addressBookController.store.subscribe((state) => { - this.store.updateState({ AddressBookController: state }) - }) - this.currencyController.store.subscribe((state) => { - this.store.updateState({ CurrencyController: state }) - }) - this.noticeController.store.subscribe((state) => { - this.store.updateState({ NoticeController: state }) - }) - this.shapeshiftController.store.subscribe((state) => { - this.store.updateState({ ShapeShiftController: state }) - }) - this.networkController.store.subscribe((state) => { - this.store.updateState({ NetworkController: state }) + this.store.updateStructure({ + TransactionController: this.txController.store, + KeyringController: this.keyringController.store, + PreferencesController: this.preferencesController.store, + AddressBookController: this.addressBookController.store, + CurrencyController: this.currencyController.store, + NoticeController: this.noticeController.store, + ShapeShiftController: this.shapeshiftController.store, + NetworkController: this.networkController.store, + InfuraController: this.infuraController.store, }) - this.infuraController.store.subscribe((state) => { - this.store.updateState({ InfuraController: state }) + this.memStore = new ComposableObservableStore(null, { + NetworkController: this.networkController.store, + AccountTracker: this.accountTracker.store, + TxController: this.txController.memStore, + BalancesController: this.balancesController.store, + TokenRatesController: this.tokenRatesController.store, + MessageManager: this.messageManager.memStore, + PersonalMessageManager: this.personalMessageManager.memStore, + TypesMessageManager: this.typedMessageManager.memStore, + KeyringController: this.keyringController.memStore, + PreferencesController: this.preferencesController.store, + RecentBlocksController: this.recentBlocksController.store, + AddressBookController: this.addressBookController.store, + CurrencyController: this.currencyController.store, + NoticeController: this.noticeController.memStore, + ShapeshiftController: this.shapeshiftController.store, + InfuraController: this.infuraController.store, }) - - // manual mem state subscriptions - const sendUpdate = this.sendUpdate.bind(this) - this.networkController.store.subscribe(sendUpdate) - this.accountTracker.store.subscribe(sendUpdate) - this.txController.memStore.subscribe(sendUpdate) - this.balancesController.store.subscribe(sendUpdate) - this.messageManager.memStore.subscribe(sendUpdate) - this.personalMessageManager.memStore.subscribe(sendUpdate) - this.typedMessageManager.memStore.subscribe(sendUpdate) - this.keyringController.memStore.subscribe(sendUpdate) - this.preferencesController.store.subscribe(sendUpdate) - this.recentBlocksController.store.subscribe(sendUpdate) - this.addressBookController.store.subscribe(sendUpdate) - this.currencyController.store.subscribe(sendUpdate) - this.noticeController.memStore.subscribe(sendUpdate) - this.shapeshiftController.store.subscribe(sendUpdate) - this.infuraController.store.subscribe(sendUpdate) + this.memStore.subscribe(this.sendUpdate.bind(this)) } /** @@ -279,6 +270,7 @@ module.exports = class MetamaskController extends EventEmitter { // memStore -> transform -> publicConfigStore this.on('update', (memState) => { + this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen const publicState = selectPublicState(memState) publicConfigStore.putState(publicState) }) @@ -308,33 +300,17 @@ module.exports = class MetamaskController extends EventEmitter { const vault = this.keyringController.store.getState().vault const isInitialized = (!!wallet || !!vault) - return extend( - { - isInitialized, - }, - this.networkController.store.getState(), - this.accountTracker.store.getState(), - this.txController.memStore.getState(), - this.messageManager.memStore.getState(), - this.personalMessageManager.memStore.getState(), - this.typedMessageManager.memStore.getState(), - this.keyringController.memStore.getState(), - this.balancesController.store.getState(), - this.preferencesController.store.getState(), - this.addressBookController.store.getState(), - this.currencyController.store.getState(), - this.noticeController.memStore.getState(), - this.infuraController.store.getState(), - this.recentBlocksController.store.getState(), - // config manager - this.configManager.getConfig(), - this.shapeshiftController.store.getState(), - { + return { + ...{ isInitialized }, + ...this.memStore.getFlatState(), + ...this.configManager.getConfig(), + ...{ lostAccounts: this.configManager.getLostAccounts(), seedWords: this.configManager.getSeedWords(), forgottenPassword: this.configManager.getPasswordForgotten(), - } - ) + isRevealingSeedWords: Boolean(this.configManager.getIsRevealingSeedWords()), + }, + } } /** @@ -372,6 +348,7 @@ module.exports = class MetamaskController extends EventEmitter { clearSeedWordCache: this.clearSeedWordCache.bind(this), resetAccount: nodeify(this.resetAccount, this), importAccountWithStrategy: this.importAccountWithStrategy.bind(this), + setIsRevealingSeedWords: this.configManager.setIsRevealingSeedWords.bind(this.configManager), // vault management submitPassword: nodeify(keyringController.submitPassword, keyringController), @@ -1057,4 +1034,12 @@ module.exports = class MetamaskController extends EventEmitter { } } + set isClientOpen (open) { + this._isClientOpen = open + this.isClientOpenAndUnlocked = this.getState().isUnlocked && open + } + + set isClientOpenAndUnlocked (active) { + this.tokenRatesController.isActive = active + } } diff --git a/app/scripts/platforms/sw.js b/app/scripts/platforms/sw.js index 007d8dc5b..56c5f2774 100644 --- a/app/scripts/platforms/sw.js +++ b/app/scripts/platforms/sw.js @@ -1,20 +1,25 @@ - class SwPlatform { - - // - // Public - // - + /** + * Reloads the platform + */ reload () { - // you cant actually do this - global.location.reload() + // TODO: you can't actually do this + /** @type {any} */ (global).location.reload() } - openWindow ({ url }) { - // this doesnt actually work - global.open(url, '_blank') + /** + * Opens a window + * @param {{url: string}} opts - The window options + */ + openWindow (opts) { + // TODO: this doesn't actually work + /** @type {any} */ (global).open(opts.url, '_blank') } + /** + * Returns the platform version + * @returns {string} + */ getVersion () { return '<unable to read version>' } diff --git a/app/scripts/platforms/window.js b/app/scripts/platforms/window.js index 1527c008b..943b2a703 100644 --- a/app/scripts/platforms/window.js +++ b/app/scripts/platforms/window.js @@ -1,18 +1,23 @@ - class WindowPlatform { - - // - // Public - // - + /** + * Reload the platform + */ reload () { - global.location.reload() + /** @type {any} */ (global).location.reload() } - openWindow ({ url }) { - global.open(url, '_blank') + /** + * Opens a window + * @param {{url: string}} opts - The window options + */ + openWindow (opts) { + /** @type {any} */ (global).open(opts.url, '_blank') } + /** + * Returns the platform version + * @returns {string} + */ getVersion () { return '<unable to read version>' } diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js index 2e4334bb1..6325b8a8d 100644 --- a/app/scripts/popup-core.js +++ b/app/scripts/popup-core.js @@ -7,10 +7,14 @@ const launchMetamaskUi = require('../../ui') const StreamProvider = require('web3-stream-provider') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex - module.exports = initializePopup - +/** + * Asynchronously initializes the MetaMask popup UI + * + * @param {{ container: Element, connectionStream: * }} config Popup configuration object + * @param {Function} cb Called when initialization is complete + */ function initializePopup ({ container, connectionStream }, cb) { // setup app async.waterfall([ @@ -19,6 +23,12 @@ function initializePopup ({ container, connectionStream }, cb) { ], cb) } +/** + * Establishes streamed connections to background scripts and a Web3 provider + * + * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection + * @param {Function} cb Called when controller connection is established + */ function connectToAccountManager (connectionStream, cb) { // setup communication with background // setup multiplexing @@ -28,6 +38,11 @@ function connectToAccountManager (connectionStream, cb) { setupWeb3Connection(mx.createStream('provider')) } +/** + * Establishes a streamed connection to a Web3 provider + * + * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection + */ function setupWeb3Connection (connectionStream) { var providerStream = new StreamProvider() providerStream.pipe(connectionStream).pipe(providerStream) @@ -38,6 +53,12 @@ function setupWeb3Connection (connectionStream) { global.eth = new Eth(providerStream) } +/** + * Establishes a streamed connection to the background account manager + * + * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection + * @param {Function} cb Called when the remote account manager connection is established + */ function setupControllerConnection (connectionStream, cb) { // this is a really sneaky way of adding EventEmitter api // to a bi-directional dnode instance diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 13c7ac5ec..bdab29c1e 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -3,12 +3,14 @@ const OldMetaMaskUiCss = require('../../old-ui/css') const NewMetaMaskUiCss = require('../../ui/css') const startPopup = require('./popup-core') const PortStream = require('./lib/port-stream.js') -const isPopupOrNotification = require('./lib/is-popup-or-notification') +const { getEnvironmentType } = require('./lib/util') +const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums') const extension = require('extensionizer') const ExtensionPlatform = require('./platforms/extension') const NotificationManager = require('./lib/notification-manager') const notificationManager = new NotificationManager() const setupRaven = require('./lib/setupRaven') +const log = require('loglevel') start().catch(log.error) @@ -26,7 +28,7 @@ async function start() { // injectCss(css) // identify window type (popup, notification) - const windowType = isPopupOrNotification() + const windowType = getEnvironmentType(window.location.href) global.METAMASK_UI_TYPE = windowType closePopupIfOpen(windowType) @@ -68,7 +70,7 @@ async function start() { function closePopupIfOpen (windowType) { - if (windowType !== 'notification') { + if (windowType !== ENVIRONMENT_TYPE_NOTIFICATION) { // should close only chrome popup notificationManager.closePopup() } |