diff options
Diffstat (limited to 'app')
34 files changed, 2100 insertions, 534 deletions
diff --git a/app/manifest.json b/app/manifest.json index dc46f1ca4..3e5eed205 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "4.5.5", + "version": "4.6.0", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/background.js b/app/scripts/background.js index 6550e8944..69d549c85 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1,3 +1,7 @@ +/** + * @file The entry point for the web extension singleton process. + */ + const urlUtil = require('url') const endOfStream = require('end-of-stream') const pump = require('pump') @@ -61,6 +65,90 @@ initialize().catch(log.error) // setup metamask mesh testing container setupMetamaskMeshMetrics() +/** + * An object representing a transaction, in whatever state it is in. + * @typedef TransactionMeta + * + * @property {number} id - An internally unique tx identifier. + * @property {number} time - Time the tx was first suggested, in unix epoch time (ms). + * @property {string} status - The current transaction status (unapproved, signed, submitted, dropped, failed, rejected), as defined in `tx-state-manager.js`. + * @property {string} metamaskNetworkId - The transaction's network ID, used for EIP-155 compliance. + * @property {boolean} loadingDefaults - TODO: Document + * @property {Object} txParams - The tx params as passed to the network provider. + * @property {Object[]} history - A history of mutations to this TransactionMeta object. + * @property {boolean} gasPriceSpecified - True if the suggesting dapp specified a gas price, prevents auto-estimation. + * @property {boolean} gasLimitSpecified - True if the suggesting dapp specified a gas limit, prevents auto-estimation. + * @property {string} estimatedGas - A hex string represented the estimated gas limit required to complete the transaction. + * @property {string} origin - A string representing the interface that suggested the transaction. + * @property {Object} nonceDetails - A metadata object containing information used to derive the suggested nonce, useful for debugging nonce issues. + * @property {string} rawTx - A hex string of the final signed transaction, ready to submit to the network. + * @property {string} hash - A hex string of the transaction hash, used to identify the transaction on the network. + * @property {number} submittedTime - The time the transaction was submitted to the network, in Unix epoch time (ms). + */ + +/** + * The data emitted from the MetaMaskController.store EventEmitter, also used to initialize the MetaMaskController. Available in UI on React state as state.metamask. + * @typedef MetaMaskState + * @property {boolean} isInitialized - Whether the first vault has been created. + * @property {boolean} isUnlocked - Whether the vault is currently decrypted and accounts are available for selection. + * @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed. + * @property {boolean} isMascara - True if the current context is the extensionless MetaMascara project. + * @property {boolean} isPopup - Returns true if the current view is an externally-triggered notification. + * @property {string} rpcTarget - DEPRECATED - The URL of the current RPC provider. + * @property {Object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys. + * @property {Object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions. + * @property {boolean} noActiveNotices - False if there are notices the user should confirm before using the application. + * @property {Array} frequentRpcList - A list of frequently used RPCs, including custom user-provided ones. + * @property {Array} addressBook - A list of previously sent to addresses. + * @property {address} selectedTokenAddress - Used to indicate if a token is globally selected. Should be deprecated in favor of UI-centric token selection. + * @property {Object} tokenExchangeRates - Info about current token prices. + * @property {Array} tokens - Tokens held by the current user, including their balances. + * @property {Object} send - TODO: Document + * @property {Object} coinOptions - TODO: Document + * @property {boolean} useBlockie - Indicates preferred user identicon format. True for blockie, false for Jazzicon. + * @property {Object} featureFlags - An object for optional feature flags. + * @property {string} networkEndpointType - TODO: Document + * @property {boolean} isRevealingSeedWords - True if seed words are currently being recovered, and should be shown to user. + * @property {boolean} welcomeScreen - True if welcome screen should be shown. + * @property {string} currentLocale - A locale string matching the user's preferred display language. + * @property {Object} provider - The current selected network provider. + * @property {string} provider.rpcTarget - The address for the RPC API, if using an RPC API. + * @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks. + * @property {string} network - A stringified number of the current network ID. + * @property {Object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values. + * @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string. + * @property {TransactionMeta[]} selectedAddressTxList - An array of transactions associated with the currently selected account. + * @property {Object} unapprovedMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options. + * @property {number} unapprovedMsgCount - The number of messages in unapprovedMsgs. + * @property {Object} unapprovedPersonalMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options. + * @property {number} unapprovedPersonalMsgCount - The number of messages in unapprovedPersonalMsgs. + * @property {Object} unapprovedTypedMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options. + * @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs. + * @property {string[]} keyringTypes - An array of unique keyring identifying strings, representing available strategies for creating accounts. + * @property {Keyring[]} keyrings - An array of keyring descriptions, summarizing the accounts that are available for use, and what keyrings they belong to. + * @property {Object} computedBalances - Maps accounts to their balances, accounting for balance changes from pending transactions. + * @property {string} currentAccountTab - A view identifying string for displaying the current displayed view, allows user to have a preferred tab in the old UI (between tokens and history). + * @property {string} selectedAddress - A lower case hex string of the currently selected address. + * @property {string} currentCurrency - A string identifying the user's preferred display currency, for use in showing conversion rates. + * @property {number} conversionRate - A number representing the current exchange rate from the user's preferred currency to Ether. + * @property {number} conversionDate - A unix epoch date (ms) for the time the current conversion rate was last retrieved. + * @property {Object} infuraNetworkStatus - An object of infura network status checks. + * @property {Block[]} recentBlocks - An array of recent blocks, used to calculate an effective but cheap gas price. + * @property {Array} shapeShiftTxList - An array of objects describing shapeshift exchange attempts. + * @property {Array} lostAccounts - TODO: Remove this feature. A leftover from the version-3 migration where our seed-phrase library changed to fix a bug where some accounts were mis-generated, but we recovered the old accounts as "lost" instead of losing them. + * @property {boolean} forgottenPassword - Returns true if the user has initiated the password recovery screen, is recovering from seed phrase. + */ + +/** + * @typedef VersionedData + * @property {MetaMaskState} data - The data emitted from MetaMask controller, or used to initialize it. + * @property {Number} version - The latest migration version that has been run. + */ + +/** + * Initializes the MetaMask controller, and sets up all platform configuration. + * @returns {Promise} Setup complete. + */ async function initialize () { const initState = await loadStateFromPersistence() const initLangCode = await getFirstPreferredLangCode() @@ -72,6 +160,11 @@ async function initialize () { // State and Persistence // +/** + * Loads any stored data, prioritizing the latest storage strategy. + * Migrates that data schema in case it was last loaded on an older version. + * @returns {Promise<MetaMaskState>} Last data emitted from previous instance of MetaMask. + */ async function loadStateFromPersistence () { // migrations const migrator = new Migrator({ migrations }) @@ -134,6 +227,16 @@ async function loadStateFromPersistence () { return versionedData.data } +/** + * Initializes the MetaMask Controller with any initial state and default language. + * Configures platform-specific error reporting strategy. + * Streams emitted state updates to platform-specific storage strategy. + * Creates platform listeners for new Dapps/Contexts, and sets up their data connections to the controller. + * + * @param {Object} initState - The initial state to start the controller with, matches the state that is emitted from the controller. + * @param {String} initLangCode - The region code for the language preferred by the current user. + * @returns {Promise} After setup is complete. + */ function setupController (initState, initLangCode) { // // MetaMask Controller @@ -158,7 +261,11 @@ function setupController (initState, initLangCode) { controller.txController.on(`tx:status-update`, (txId, status) => { if (status !== 'failed') return const txMeta = controller.txController.txStateManager.getTx(txId) - reportFailedTxToSentry({ raven, txMeta }) + try { + reportFailedTxToSentry({ raven, txMeta }) + } catch (e) { + console.error(e) + } }) // setup state persistence @@ -172,6 +279,11 @@ function setupController (initState, initLangCode) { } ) + /** + * Assigns the given state to the versioned object (with metadata), and returns that. + * @param {Object} state - The state object as emitted by the MetaMaskController. + * @returns {VersionedData} The state object wrapped in an object that includes a metadata key. + */ function versionifyData (state) { versionedData.data = state return versionedData @@ -208,6 +320,18 @@ function setupController (initState, initLangCode) { return popupIsOpen || Boolean(Object.keys(openMetamaskTabsIDs).length) || notificationIsOpen } + /** + * A runtime.Port object, as provided by the browser: + * @link https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/Port + * @typedef Port + * @type Object + */ + + /** + * Connects a Port to the MetaMask controller via a multiplexed duplex stream. + * This method identifies trusted (MetaMask) interfaces, and connects them differently from untrusted (web pages). + * @param {Port} remotePort - The port provided by a new context. + */ function connectRemote (remotePort) { const processName = remotePort.name const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName] @@ -261,7 +385,10 @@ function setupController (initState, initLangCode) { controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) - // plugin badge text + /** + * Updates the Web Extension's "badge" number, on the little fox in the toolbar. + * The number reflects the current number of pending transactions or message signatures needing user approval. + */ function updateBadge () { var label = '' var unapprovedTxCount = controller.txController.getUnapprovedTxCount() @@ -283,7 +410,9 @@ function setupController (initState, initLangCode) { // Etc... // -// popup trigger +/** + * Opens the browser popup for user confirmation + */ function triggerUi () { extension.tabs.query({ active: true }, tabs => { const currentlyActiveMetamaskTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id])) diff --git a/app/scripts/config.js b/app/scripts/config.js deleted file mode 100644 index e6f70ca2b..000000000 --- a/app/scripts/config.js +++ /dev/null @@ -1,79 +0,0 @@ -const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask' -const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask' -const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' -const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' -const LOCALHOST_RPC_URL = 'http://localhost:8545' - -const MAINET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2' -const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2' -const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2' -const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2' - -const DEFAULT_RPC = 'rinkeby' -const OLD_UI_NETWORK_TYPE = 'network' -const BETA_UI_NETWORK_TYPE = 'networkBeta' - -global.METAMASK_DEBUG = process.env.METAMASK_DEBUG - -/** - * @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, - ropsten: ROPSTEN_RPC_URL, - kovan: KOVAN_RPC_URL, - rinkeby: RINKEBY_RPC_URL, - }, - networkBeta: { - localhost: LOCALHOST_RPC_URL, - mainnet: MAINET_RPC_URL_BETA, - ropsten: ROPSTEN_RPC_URL_BETA, - kovan: KOVAN_RPC_URL_BETA, - rinkeby: RINKEBY_RPC_URL_BETA, - }, - networkNames: { - 3: 'Ropsten', - 4: 'Rinkeby', - 42: 'Kovan', - }, - enums: { - DEFAULT_RPC, - OLD_UI_NETWORK_TYPE, - BETA_UI_NETWORK_TYPE, - }, -} - -module.exports = config diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index f83f294cc..86619fce1 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -4,6 +4,24 @@ const BN = require('ethereumjs-util').BN class BalanceController { + /** + * Controller responsible for storing and updating an account's balance. + * + * @typedef {Object} BalanceController + * @param {Object} opts Initialize various properties of the class. + * @property {string} address A base 16 hex string. The account address which has the balance managed by this + * BalanceController. + * @property {AccountTracker} accountTracker Stores and updates the users accounts + * for which this BalanceController manages balance. + * @property {TransactionController} txController Stores, tracks and manages transactions. Here used to create a listener for + * transaction updates. + * @property {BlockTracker} blockTracker Tracks updates to blocks. On new blocks, this BalanceController updates its balance + * @property {Object} store The store for the ethBalance + * @property {string} store.ethBalance A base 16 hex string. The balance for the current account. + * @property {PendingBalanceCalculator} balanceCalc Used to calculate the accounts balance with possible pending + * transaction costs taken into account. + * + */ constructor (opts = {}) { this._validateParams(opts) const { address, accountTracker, txController, blockTracker } = opts @@ -26,6 +44,11 @@ class BalanceController { this._registerUpdates() } + /** + * Updates the ethBalance property to the current pending balance + * + * @returns {Promise<void>} Promises undefined + */ async updateBalance () { const balance = await this.balanceCalc.getBalance() this.store.updateState({ @@ -33,6 +56,15 @@ class BalanceController { }) } + /** + * Sets up listeners and subscriptions which should trigger an update of ethBalance. These updates include: + * - when a transaction changes state to 'submitted', 'confirmed' or 'failed' + * - when the current account changes (i.e. a new account is selected) + * - when there is a block update + * + * @private + * + */ _registerUpdates () { const update = this.updateBalance.bind(this) @@ -51,6 +83,14 @@ class BalanceController { this.blockTracker.on('block', update) } + /** + * Gets the balance, as a base 16 hex string, of the account at this BalanceController's current address. + * If the current account has no balance, returns undefined. + * + * @returns {Promise<BN|void>} Promises a BN with a value equal to the balance of the current account, or undefined + * if the current account has no balance + * + */ async _getBalance () { const { accounts } = this.accountTracker.store.getState() const entry = accounts[this.address] @@ -58,6 +98,14 @@ class BalanceController { return balance ? new BN(balance.substring(2), 16) : undefined } + /** + * Gets the pending transactions (i.e. those with a 'submitted' status). These are accessed from the + * TransactionController passed to this BalanceController during construction. + * + * @private + * @returns {Promise<array>} Promises an array of transaction objects. + * + */ async _getPendingTransactions () { const pending = this.txController.getFilteredTxList({ from: this.address, @@ -67,6 +115,14 @@ class BalanceController { return pending } + /** + * Validates that the passed options have all required properties. + * + * @param {Object} opts The options object to validate + * @throws {string} Throw a custom error indicating that address, accountTracker, txController and blockTracker are + * missing and at least one is required + * + */ _validateParams (opts) { const { address, accountTracker, txController, blockTracker } = opts if (!address || !accountTracker || !txController || !blockTracker) { diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index d965f80b8..f100c4525 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -10,6 +10,22 @@ 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, @@ -22,16 +38,28 @@ class BlacklistController { this._phishingUpdateIntervalRef = null } - // - // PUBLIC METHODS - // - + /** + * 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 { 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 () { const response = await fetch('https://api.infura.io/v2/blacklist') const phishing = await response.json() @@ -40,6 +68,11 @@ class BlacklistController { 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().catch(log.warn) @@ -48,10 +81,14 @@ class BlacklistController { }, POLLING_INTERVAL) } - // - // PRIVATE METHODS - // - + /** + * 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) } diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js new file mode 100644 index 000000000..4f29e301b --- /dev/null +++ b/app/scripts/controllers/network/enums.js @@ -0,0 +1,56 @@ +const ROPSTEN = 'ropsten' +const RINKEBY = 'rinkeby' +const KOVAN = 'kovan' +const MAINNET = 'mainnet' +const LOCALHOST = 'localhost' + +const ROPSTEN_CODE = 3 +const RINKEYBY_CODE = 4 +const KOVAN_CODE = 42 + +const ROPSTEN_DISPLAY_NAME = 'Ropsten' +const RINKEBY_DISPLAY_NAME = 'Rinkeby' +const KOVAN_DISPLAY_NAME = 'Kovan' +const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' + +const MAINNET_RPC_URL = 'https://mainnet.infura.io/metamask' +const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask' +const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' +const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' +const LOCALHOST_RPC_URL = 'http://localhost:8545' + +const MAINNET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2' +const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2' +const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2' +const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2' + +const DEFAULT_NETWORK = 'rinkeby' +const OLD_UI_NETWORK_TYPE = 'network' +const BETA_UI_NETWORK_TYPE = 'networkBeta' + +module.exports = { + ROPSTEN, + RINKEBY, + KOVAN, + MAINNET, + LOCALHOST, + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, + ROPSTEN_DISPLAY_NAME, + RINKEBY_DISPLAY_NAME, + KOVAN_DISPLAY_NAME, + MAINNET_DISPLAY_NAME, + MAINNET_RPC_URL, + ROPSTEN_RPC_URL, + KOVAN_RPC_URL, + RINKEBY_RPC_URL, + LOCALHOST_RPC_URL, + MAINNET_RPC_URL_BETA, + ROPSTEN_RPC_URL_BETA, + KOVAN_RPC_URL_BETA, + RINKEBY_RPC_URL_BETA, + DEFAULT_NETWORK, + OLD_UI_NETWORK_TYPE, + BETA_UI_NETWORK_TYPE, +} diff --git a/app/scripts/controllers/network/index.js b/app/scripts/controllers/network/index.js new file mode 100644 index 000000000..fb095bf33 --- /dev/null +++ b/app/scripts/controllers/network/index.js @@ -0,0 +1,2 @@ +const NetworkController = require('./network') +module.exports = NetworkController diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network/network.js index 45574e673..2f5b81cd2 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,17 +1,24 @@ const assert = require('assert') const EventEmitter = require('events') const createMetamaskProvider = require('web3-provider-engine/zero.js') -const SubproviderFromProvider = require('web3-provider-engine/subproviders/web3.js') +const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js') const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') const EthQuery = require('eth-query') -const createEventEmitterProxy = require('../lib/events-proxy.js') -const networkConfig = require('../config.js') +const createEventEmitterProxy = require('../../lib/events-proxy.js') const log = require('loglevel') -const { OLD_UI_NETWORK_TYPE, DEFAULT_RPC } = networkConfig.enums -const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet'] +const { + ROPSTEN, + RINKEBY, + KOVAN, + MAINNET, + OLD_UI_NETWORK_TYPE, + DEFAULT_NETWORK, +} = require('./enums') +const { getNetworkEndpoints } = require('./util') +const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] module.exports = class NetworkController extends EventEmitter { @@ -19,8 +26,8 @@ module.exports = class NetworkController extends EventEmitter { super() this._networkEndpointVersion = OLD_UI_NETWORK_TYPE - this._networkEndpoints = this.getNetworkEndpoints(OLD_UI_NETWORK_TYPE) - this._defaultRpc = this._networkEndpoints[DEFAULT_RPC] + this._networkEndpoints = getNetworkEndpoints(OLD_UI_NETWORK_TYPE) + this._defaultRpc = this._networkEndpoints[DEFAULT_NETWORK] config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) this.networkStore = new ObservableStore('loading') @@ -37,17 +44,13 @@ module.exports = class NetworkController extends EventEmitter { } this._networkEndpointVersion = version - this._networkEndpoints = this.getNetworkEndpoints(version) - this._defaultRpc = this._networkEndpoints[DEFAULT_RPC] + this._networkEndpoints = getNetworkEndpoints(version) + this._defaultRpc = this._networkEndpoints[DEFAULT_NETWORK] const { type } = this.getProviderConfig() return this.setProviderType(type, true) } - getNetworkEndpoints (version = OLD_UI_NETWORK_TYPE) { - return networkConfig[version] - } - initializeProvider (_providerParams) { this._baseProviderParams = _providerParams const { type, rpcTarget } = this.providerStore.getState() diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js new file mode 100644 index 000000000..4f38ccda4 --- /dev/null +++ b/app/scripts/controllers/network/util.js @@ -0,0 +1,65 @@ +const { + ROPSTEN, + RINKEBY, + KOVAN, + MAINNET, + LOCALHOST, + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, + ROPSTEN_DISPLAY_NAME, + RINKEBY_DISPLAY_NAME, + KOVAN_DISPLAY_NAME, + MAINNET_DISPLAY_NAME, + MAINNET_RPC_URL, + ROPSTEN_RPC_URL, + KOVAN_RPC_URL, + RINKEBY_RPC_URL, + LOCALHOST_RPC_URL, + MAINNET_RPC_URL_BETA, + ROPSTEN_RPC_URL_BETA, + KOVAN_RPC_URL_BETA, + RINKEBY_RPC_URL_BETA, + OLD_UI_NETWORK_TYPE, + BETA_UI_NETWORK_TYPE, +} = require('./enums') + +const networkToNameMap = { + [ROPSTEN]: ROPSTEN_DISPLAY_NAME, + [RINKEBY]: RINKEBY_DISPLAY_NAME, + [KOVAN]: KOVAN_DISPLAY_NAME, + [MAINNET]: MAINNET_DISPLAY_NAME, + [ROPSTEN_CODE]: ROPSTEN_DISPLAY_NAME, + [RINKEYBY_CODE]: RINKEBY_DISPLAY_NAME, + [KOVAN_CODE]: KOVAN_DISPLAY_NAME, +} + +const networkEndpointsMap = { + [OLD_UI_NETWORK_TYPE]: { + [LOCALHOST]: LOCALHOST_RPC_URL, + [MAINNET]: MAINNET_RPC_URL, + [ROPSTEN]: ROPSTEN_RPC_URL, + [KOVAN]: KOVAN_RPC_URL, + [RINKEBY]: RINKEBY_RPC_URL, + }, + [BETA_UI_NETWORK_TYPE]: { + [LOCALHOST]: LOCALHOST_RPC_URL, + [MAINNET]: MAINNET_RPC_URL_BETA, + [ROPSTEN]: ROPSTEN_RPC_URL_BETA, + [KOVAN]: KOVAN_RPC_URL_BETA, + [RINKEBY]: RINKEBY_RPC_URL_BETA, + }, +} + +const getNetworkDisplayName = key => networkToNameMap[key] + +const getNetworkEndpoints = (networkType = OLD_UI_NETWORK_TYPE) => { + return { + ...networkEndpointsMap[networkType], + } +} + +module.exports = { + getNetworkDisplayName, + getNetworkEndpoints, +} diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index d4d508026..1d3308d36 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -8,8 +8,8 @@ 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 {object} store The stored 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 diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index 0c1ee4e38..1377c1ba9 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -6,6 +6,23 @@ const log = require('loglevel') class RecentBlocksController { + /** + * Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled + * upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event + * (indicating that there is a new block to process). + * + * @typedef {Object} RecentBlocksController + * @param {object} opts Contains objects necessary for tracking blocks and querying the blockchain + * @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain + * @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance. + * @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction, + * listens for 'block' events so that new blocks can be processed and added to storage. + * @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider + * @property {number} historyLength The maximum length of blocks to track + * @property {object} store Stores the recentBlocks + * @property {array} store.recentBlocks Contains all recent blocks, up to a total that is equal to this.historyLength + * + */ constructor (opts = {}) { const { blockTracker, provider } = opts this.blockTracker = blockTracker @@ -21,12 +38,23 @@ class RecentBlocksController { this.backfill() } + /** + * Sets store.recentBlocks to an empty array + * + */ resetState () { this.store.updateState({ recentBlocks: [], }) } + /** + * Receives a new block and modifies it with this.mapTransactionsToPrices. Then adds that block to the recentBlocks + * array in storage. If the recentBlocks array contains the maximum number of blocks, the oldest block is removed. + * + * @param {object} newBlock The new block to modify and add to the recentBlocks array + * + */ processBlock (newBlock) { const block = this.mapTransactionsToPrices(newBlock) @@ -40,6 +68,15 @@ class RecentBlocksController { this.store.updateState(state) } + /** + * Receives a new block and modifies it with this.mapTransactionsToPrices. Adds that block to the recentBlocks + * array in storage, but only if the recentBlocks array contains fewer than the maximum permitted. + * + * Unlike this.processBlock, backfillBlock adds the modified new block to the beginning of the recent block array. + * + * @param {object} newBlock The new block to modify and add to the beginning of the recentBlocks array + * + */ backfillBlock (newBlock) { const block = this.mapTransactionsToPrices(newBlock) @@ -52,6 +89,14 @@ class RecentBlocksController { this.store.updateState(state) } + /** + * Receives a block and gets the gasPrice of each of its transactions. These gas prices are added to the block at a + * new property, and the block's transactions are removed. + * + * @param {object} newBlock The block to modify. It's transaction array will be replaced by a gasPrices array. + * @returns {object} The modified block. + * + */ mapTransactionsToPrices (newBlock) { const block = extend(newBlock, { gasPrices: newBlock.transactions.map((tx) => { @@ -62,6 +107,16 @@ class RecentBlocksController { return block } + /** + * On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks + * array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first + * 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying + * the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest. + * + * Each iteration over the block numbers is delayed by 100 milliseconds. + * + * @returns {Promise<void>} Promises undefined + */ async backfill() { this.blockTracker.once('block', async (block) => { let blockNum = block.number @@ -90,12 +145,25 @@ class RecentBlocksController { }) } + /** + * A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await + * + * @returns {Promise<void>} Promises undefined + * + */ async wait () { return new Promise((resolve) => { setTimeout(resolve, 100) }) } + /** + * Uses EthQuery to get a block that has a given block number. + * + * @param {number} number The number of the block to get + * @returns {Promise<object>} Promises A block with the passed number + * + */ async getBlockByNumber (number) { const bn = new BN(number) return new Promise((resolve, reject) => { diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 28409ea10..87d716aa6 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,4 +1,5 @@ const ObservableStore = require('obs-store') +const { warn } = require('loglevel') // By default, poll every 3 minutes const DEFAULT_INTERVAL = 180 * 1000 @@ -39,10 +40,13 @@ class TokenRatesController { */ async fetchExchangeRate (address) { try { - const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`) + const response = await fetch(`https://metamask.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) { } + } catch (error) { + warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error) + return 0 + } } /** diff --git a/app/scripts/controllers/transactions/README.md b/app/scripts/controllers/transactions/README.md new file mode 100644 index 000000000..b414762dc --- /dev/null +++ b/app/scripts/controllers/transactions/README.md @@ -0,0 +1,92 @@ +# Transaction Controller + +Transaction Controller is an aggregate of sub-controllers and trackers +exposed to the MetaMask controller. + +- txStateManager + responsible for the state of a transaction and + storing the transaction +- pendingTxTracker + watching blocks for transactions to be include + and emitting confirmed events +- txGasUtil + gas calculations and safety buffering +- nonceTracker + calculating nonces + +## Flow diagram of processing a transaction + +![transaction-flow](../../../../docs/transaction-flow.png) + +## txMeta's & txParams + +A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must +be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta! + +Here is a txMeta too look at: + +```js +txMeta = { + "id": 2828415030114568, // unique id for this txMeta used for look ups + "time": 1524094064821, // time of creation + "status": "confirmed", + "metamaskNetworkId": "1524091532133", //the network id for the transaction + "loadingDefaults": false, // used to tell the ui when we are done calculatyig gass defaults + "txParams": { // the txParams object + "from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "value": "0x0", + "gasPrice": "0x3b9aca00", + "gas": "0x7b0c", + "nonce": "0x0" + }, + "history": [{ //debug + "id": 2828415030114568, + "time": 1524094064821, + "status": "unapproved", + "metamaskNetworkId": "1524091532133", + "loadingDefaults": true, + "txParams": { + "from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "value": "0x0" + } + }, + [ + { + "op": "add", + "path": "/txParams/gasPrice", + "value": "0x3b9aca00" + }, + ...], // I've removed most of history for this + "gasPriceSpecified": false, //whether or not the user/dapp has specified gasPrice + "gasLimitSpecified": false, //whether or not the user/dapp has specified gas + "estimatedGas": "5208", + "origin": "MetaMask", //debug + "nonceDetails": { + "params": { + "highestLocallyConfirmed": 0, + "highestSuggested": 0, + "nextNetworkNonce": 0 + }, + "local": { + "name": "local", + "nonce": 0, + "details": { + "startPoint": 0, + "highest": 0 + } + }, + "network": { + "name": "network", + "nonce": 0, + "details": { + "baseCount": 0 + } + } + }, + "rawTx": "0xf86980843b9aca00827b0c948acce2391c0d510a6c5e5d8f819a678f79b7e67580808602c5b5de66eea05c01a320b96ac730cb210ca56d2cb71fa360e1fc2c21fa5cf333687d18eb323fa02ed05987a6e5fd0f2459fcff80710b76b83b296454ad9a37594a0ccb4643ea90", // used for rebroadcast + "hash": "0xa45ba834b97c15e6ff4ed09badd04ecd5ce884b455eb60192cdc73bcc583972a", + "submittedTime": 1524094077902 // time of the attempt to submit the raw tx to the network, used in the ui to show the retry button +} +``` diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions/index.js index c8211ebd7..541f1db73 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,28 +3,42 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') -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 TransactionStateManager = require('./tx-state-manager') +const TxGasUtil = require('./tx-gas-utils') +const PendingTransactionTracker = require('./pending-tx-tracker') +const NonceTracker = require('./nonce-tracker') +const txUtils = require('./lib/util') const log = require('loglevel') -/* +/** Transaction Controller is an aggregate of sub-controllers and trackers composing them in a way to be exposed to the metamask controller - - txStateManager + <br>- txStateManager responsible for the state of a transaction and storing the transaction - - pendingTxTracker + <br>- pendingTxTracker watching blocks for transactions to be include and emitting confirmed events - - txGasUtil + <br>- txGasUtil gas calculations and safety buffering - - nonceTracker + <br>- nonceTracker calculating nonces + + + @class + @param {object} - opts + @param {object} opts.initState - initial transaction list default is an empty array + @param {Object} opts.networkStore - an observable store for network number + @param {Object} opts.blockTracker - An instance of eth-blocktracker + @param {Object} opts.provider - A network provider. + @param {Function} opts.signTransaction - function the signs an ethereumjs-tx + @param {Function} [opts.getGasPrice] - optional gas price calculator + @param {Function} opts.signTransaction - ethTx signer that returns a rawTx + @param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state + @param {Object} opts.preferencesStore */ -module.exports = class TransactionController extends EventEmitter { +class TransactionController extends EventEmitter { constructor (opts) { super() this.networkStore = opts.networkStore || new ObservableStore({}) @@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter { this.query = new EthQuery(this.provider) this.txGasUtil = new TxGasUtil(this.provider) + this._mapMethods() this.txStateManager = new TransactionStateManager({ initState: opts.initState, txHistoryLimit: opts.txHistoryLimit, getNetwork: this.getNetwork.bind(this), }) - - this.txStateManager.getFilteredTxList({ - status: 'unapproved', - loadingDefaults: true, - }).forEach((tx) => { - this.addTxDefaults(tx) - .then((txMeta) => { - txMeta.loadingDefaults = false - this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') - }).catch((error) => { - this.txStateManager.setTxStatusFailed(tx.id, error) - }) - }) - - this.txStateManager.getFilteredTxList({ - status: 'approved', - }).forEach((txMeta) => { - const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') - this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) - }) - + this._onBootCleanUp() this.store = this.txStateManager.store - this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), - getConfirmedTransactions: (address) => { - return this.txStateManager.getFilteredTxList({ - from: address, - status: 'confirmed', - err: undefined, - }) - }, + getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) this.pendingTxTracker = new PendingTransactionTracker({ @@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter { }) this.txStateManager.store.subscribe(() => this.emit('update:badge')) - - this.pendingTxTracker.on('tx:warning', (txMeta) => { - this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') - }) - this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) - this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) - this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { - if (!txMeta.firstRetryBlockNumber) { - txMeta.firstRetryBlockNumber = latestBlockNumber - this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update') - } - }) - this.pendingTxTracker.on('tx:retry', (txMeta) => { - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 - txMeta.retryCount++ - this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') - }) - - this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) - // this is a little messy but until ethstore has been either - // removed or redone this is to guard against the race condition - this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) - this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) + this._setupListners() // memstore is computed from a few different stores this._updateMemstore() this.txStateManager.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) } - - getState () { - return this.memStore.getState() - } - - getNetwork () { - return this.networkStore.getState() - } - - getSelectedAddress () { - return this.preferencesStore.getState().selectedAddress - } - - getUnapprovedTxCount () { - return Object.keys(this.txStateManager.getUnapprovedTxList()).length - } - - getPendingTxCount (account) { - return this.txStateManager.getPendingTransactions(account).length - } - - getFilteredTxList (opts) { - return this.txStateManager.getFilteredTxList(opts) - } - + /** @returns {number} the chainId*/ getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState) @@ -152,16 +94,30 @@ module.exports = class TransactionController extends EventEmitter { } } - wipeTransactions (address) { - this.txStateManager.wipeTransactions(address) - } - - // Adds a tx to the txlist +/** + Adds a tx to the txlist + @emits ${txMeta.id}:unapproved +*/ addTx (txMeta) { this.txStateManager.addTx(txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta) } + /** + Wipes the transactions for a given account + @param {string} address - hex string of the from address for txs being removed + */ + wipeTransactions (address) { + this.txStateManager.wipeTransactions(address) + } + + /** + add a new unapproved transaction to the pipeline + + @returns {Promise<string>} the hash of the transaction after being submitted to the network + @param txParams {object} - txParams for the transaction + @param opts {object} - with the key origin to put the origin on the txMeta + */ async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const initialTxMeta = await this.addUnapprovedTransaction(txParams) @@ -184,17 +140,24 @@ module.exports = class TransactionController extends EventEmitter { }) } + /** + Validates and generates a txMeta with defaults and puts it in txStateManager + store + + @returns {txMeta} + */ + async addUnapprovedTransaction (txParams) { // validate - const normalizedTxParams = this._normalizeTxParams(txParams) - this._validateTxParams(normalizedTxParams) + const normalizedTxParams = txUtils.normalizeTxParams(txParams) + txUtils.validateTxParams(normalizedTxParams) // construct txMeta let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) // add default tx params try { - txMeta = await this.addTxDefaults(txMeta) + txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { console.log(error) this.txStateManager.setTxStatusFailed(txMeta.id, error) @@ -206,21 +169,33 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } - - async addTxDefaults (txMeta) { +/** + adds the tx gas defaults: gas && gasPrice + @param txMeta {Object} - the txMeta object + @returns {Promise<object>} resolves with txMeta +*/ + async addTxGasDefaults (txMeta) { const txParams = txMeta.txParams // ensure value + txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0' txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) let gasPrice = txParams.gasPrice if (!gasPrice) { gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice() } txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) - txParams.value = txParams.value || '0x0' // set gasLimit return await this.txGasUtil.analyzeGasUsage(txMeta) } + /** + Creates a new txMeta with the same txParams as the original + to allow the user to resign the transaction with a higher gas values + @param originalTxId {number} - the id of the txMeta that + you want to attempt to retry + @return {txMeta} + */ + async retryTransaction (originalTxId) { const originalTxMeta = this.txStateManager.getTx(originalTxId) const lastGasPrice = originalTxMeta.txParams.gasPrice @@ -234,15 +209,31 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } + /** + updates the txMeta in the txStateManager + @param txMeta {Object} - the updated txMeta + */ async updateTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') } + /** + updates and approves the transaction + @param txMeta {Object} + */ async updateAndApproveTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') await this.approveTransaction(txMeta.id) } + /** + sets the tx status to approved + auto fills the nonce + signs the transaction + publishes the transaction + if any of these steps fails the tx status will be set to failed + @param txId {number} - the tx's Id + */ async approveTransaction (txId) { let nonceLock try { @@ -274,7 +265,11 @@ module.exports = class TransactionController extends EventEmitter { throw err } } - + /** + adds the chain id and signs the transaction and set the status to signed + @param txId {number} - the tx's Id + @returns - rawTx {string} + */ async signTransaction (txId) { const txMeta = this.txStateManager.getTx(txId) // add network/chain id @@ -290,6 +285,12 @@ module.exports = class TransactionController extends EventEmitter { return rawTx } + /** + publishes the raw tx and sets the txMeta to submitted + @param txId {number} - the tx's Id + @param rawTx {string} - the hex string of the serialized signed transaction + @returns {Promise<void>} + */ async publishTransaction (txId, rawTx) { const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx @@ -299,11 +300,20 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } + /** + Convenience method for the ui thats sets the transaction to rejected + @param txId {number} - the tx's Id + @returns {Promise<void>} + */ async cancelTransaction (txId) { this.txStateManager.setTxStatusRejected(txId) } - // receives a txHash records the tx as signed + /** + Sets the txHas on the txMeta + @param txId {number} - the tx's Id + @param txHash {string} - the hash for the txMeta + */ setTxHash (txId, txHash) { // Add the tx hash to the persisted meta-tx object const txMeta = this.txStateManager.getTx(txId) @@ -314,63 +324,92 @@ module.exports = class TransactionController extends EventEmitter { // // PRIVATE METHODS // + /** maps methods for convenience*/ + _mapMethods () { + /** @returns the state in transaction controller */ + this.getState = () => this.memStore.getState() + /** @returns the network number stored in networkStore */ + this.getNetwork = () => this.networkStore.getState() + /** @returns the user selected address */ + this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress + /** Returns an array of transactions whos status is unapproved */ + this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length + /** + @returns a number that represents how many transactions have the status submitted + @param account {String} - hex prefixed account + */ + this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length + /** see txStateManager */ + this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) + } - _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(), - nonce: nonce => ethUtil.addHexPrefix(nonce), - value: value => ethUtil.addHexPrefix(value), - data: data => ethUtil.addHexPrefix(data), - gas: gas => ethUtil.addHexPrefix(gas), - gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice), - } + /** + If transaction controller was rebooted with transactions that are uncompleted + in steps of the transaction signing or user confirmation process it will either + transition txMetas to a failed state or try to redo those tasks. + */ - // apply only keys in the whiteList - const normalizedTxParams = {} - Object.keys(whiteList).forEach((key) => { - if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) + _onBootCleanUp () { + this.txStateManager.getFilteredTxList({ + status: 'unapproved', + loadingDefaults: true, + }).forEach((tx) => { + this.addTxGasDefaults(tx) + .then((txMeta) => { + txMeta.loadingDefaults = false + this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') + }).catch((error) => { + this.txStateManager.setTxStatusFailed(tx.id, error) + }) }) - return normalizedTxParams + this.txStateManager.getFilteredTxList({ + status: 'approved', + }).forEach((txMeta) => { + const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') + this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) + }) } - _validateTxParams (txParams) { - this._validateFrom(txParams) - this._validateRecipient(txParams) - if ('value' in txParams) { - const value = txParams.value.toString() - if (value.includes('-')) { - throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) + /** + is called in constructor applies the listeners for pendingTxTracker txStateManager + and blockTracker + */ + _setupListners () { + this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) + this.pendingTxTracker.on('tx:warning', (txMeta) => { + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') + }) + this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId)) + this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) + this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { + if (!txMeta.firstRetryBlockNumber) { + txMeta.firstRetryBlockNumber = latestBlockNumber + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update') } + }) + this.pendingTxTracker.on('tx:retry', (txMeta) => { + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + txMeta.retryCount++ + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') + }) - if (value.includes('.')) { - throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`) - } - } - } + this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) + // this is a little messy but until ethstore has been either + // removed or redone this is to guard against the race condition + this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) + this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) - _validateFrom (txParams) { - if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`) - if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address') } - _validateRecipient (txParams) { - if (txParams.to === '0x' || txParams.to === null ) { - if (txParams.data) { - delete txParams.to - } else { - throw new Error('Invalid recipient address') - } - } else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) { - throw new Error('Invalid recipient address') - } - return txParams - } + /** + Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions + in the list have the same nonce + @param txId {Number} - the txId of the transaction that has been confirmed in a block + */ _markNonceDuplicatesDropped (txId) { - this.txStateManager.setTxStatusConfirmed(txId) // get the confirmed transactions nonce and from address const txMeta = this.txStateManager.getTx(txId) const { nonce, from } = txMeta.txParams @@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter { }) } + /** + Updates the memStore in transaction controller + */ _updateMemstore () { const unapprovedTxs = this.txStateManager.getUnapprovedTxList() const selectedAddressTxList = this.txStateManager.getFilteredTxList({ @@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter { this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } } + +module.exports = TransactionController diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 94c7b6792..59a4b562c 100644 --- a/app/scripts/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -1,6 +1,6 @@ const jsonDiffer = require('fast-json-patch') const clone = require('clone') - +/** @module*/ module.exports = { generateHistoryEntry, replayHistory, @@ -8,7 +8,11 @@ module.exports = { migrateFromSnapshotsToDiffs, } - +/** + converts non-initial history entries into diffs + @param longHistory {array} + @returns {array} +*/ function migrateFromSnapshotsToDiffs (longHistory) { return ( longHistory @@ -20,6 +24,17 @@ function migrateFromSnapshotsToDiffs (longHistory) { ) } +/** + generates an array of history objects sense the previous state. + The object has the keys opp(the operation preformed), + path(the key and if a nested object then each key will be seperated with a `/`) + value + with the first entry having the note + @param previousState {object} - the previous state of the object + @param newState {object} - the update object + @param note {string} - a optional note for the state change + @reurns {array} +*/ function generateHistoryEntry (previousState, newState, note) { const entry = jsonDiffer.compare(previousState, newState) // Add a note to the first op, since it breaks if we append it to the entry @@ -27,11 +42,19 @@ function generateHistoryEntry (previousState, newState, note) { return entry } +/** + Recovers previous txMeta state obj + @return {object} +*/ function replayHistory (_shortHistory) { const shortHistory = clone(_shortHistory) return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) } +/** + @param txMeta {Object} + @returns {object} a clone object of the txMeta with out history +*/ function snapshotFromTxMeta (txMeta) { // create txMeta snapshot for history const snapshot = clone(txMeta) diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js new file mode 100644 index 000000000..84f7592a0 --- /dev/null +++ b/app/scripts/controllers/transactions/lib/util.js @@ -0,0 +1,99 @@ +const { + addHexPrefix, + isValidAddress, +} = require('ethereumjs-util') + +/** +@module +*/ +module.exports = { + normalizeTxParams, + validateTxParams, + validateFrom, + validateRecipient, + getFinalStates, +} + + +// functions that handle normalizing of that key in txParams +const normalizers = { + from: from => addHexPrefix(from).toLowerCase(), + to: to => addHexPrefix(to).toLowerCase(), + nonce: nonce => addHexPrefix(nonce), + value: value => addHexPrefix(value), + data: data => addHexPrefix(data), + gas: gas => addHexPrefix(gas), + gasPrice: gasPrice => addHexPrefix(gasPrice), +} + + /** + normalizes txParams + @param txParams {object} + @returns {object} normalized txParams + */ +function normalizeTxParams (txParams) { + // apply only keys in the normalizers + const normalizedTxParams = {} + for (const key in normalizers) { + if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key]) + } + return normalizedTxParams +} + + /** + validates txParams + @param txParams {object} + */ +function validateTxParams (txParams) { + validateFrom(txParams) + validateRecipient(txParams) + if ('value' in txParams) { + const value = txParams.value.toString() + if (value.includes('-')) { + throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) + } + + if (value.includes('.')) { + throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`) + } + } +} + + /** + validates the from field in txParams + @param txParams {object} + */ +function validateFrom (txParams) { + if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`) + if (!isValidAddress(txParams.from)) throw new Error('Invalid from address') +} + + /** + validates the to field in txParams + @param txParams {object} + */ +function validateRecipient (txParams) { + if (txParams.to === '0x' || txParams.to === null) { + if (txParams.data) { + delete txParams.to + } else { + throw new Error('Invalid recipient address') + } + } else if (txParams.to !== undefined && !isValidAddress(txParams.to)) { + throw new Error('Invalid recipient address') + } + return txParams +} + + /** + @returns an {array} of states that can be considered final + */ +function getFinalStates () { + return [ + 'rejected', // the user has responded no! + 'confirmed', // the tx has been included in a block. + 'failed', // the tx failed for some reason, included on tx data. + 'dropped', // the tx nonce was already used + ] +} + diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index 5b1cd7f43..f8cdc5523 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -1,7 +1,15 @@ 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, getPendingTransactions, getConfirmedTransactions }) { @@ -12,6 +20,9 @@ class NonceTracker { this.lockMap = {} } + /** + @returns {Promise<Object>} with the key releaseLock (the gloabl mutex) + */ async getGlobalLock () { const globalMutex = this._lookupMutex('global') // await global mutex free @@ -19,8 +30,20 @@ class NonceTracker { return { releaseLock } } - // releaseLock must be called - // releaseLock must be called after adding signed tx to pending transactions (or discarding) + /** + * @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} highetSuggested - 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() @@ -123,6 +146,17 @@ class NonceTracker { 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 @@ -140,6 +174,10 @@ class NonceTracker { // this is a hotfix for the fact that the blockTracker will // change when the network changes + + /** + @returns {Object} the current blockTracker + */ _getBlockTracker () { return this.provider._blockTracker } diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index e8869e6b8..6e2fcb40b 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,23 +1,24 @@ const EventEmitter = require('events') +const log = require('loglevel') const EthQuery = require('ethjs-query') -/* - - Utility class for tracking the transactions as they - go from a pending state to a confirmed (mined in a block) state +/** + Event emitter utility class for tracking the transactions as they<br> + go from a pending state to a confirmed (mined in a block) state<br> +<br> As well as continues broadcast while in the pending state +<br> +@param config {object} - non optional configuration object consists of: + @param {Object} config.provider - A network provider. + @param {Object} config.nonceTracker see nonce tracker + @param {function} config.getPendingTransactions a function for getting an array of transactions, + @param {function} config.publishTransaction a async function for publishing raw transactions, - ~config is not optional~ - requires a: { - provider: //, - nonceTracker: //see nonce tracker, - getPendingTransactions: //() a function for getting an array of transactions, - publishTransaction: //(rawTx) a async function for publishing raw transactions, - } +@class */ -module.exports = class PendingTransactionTracker extends EventEmitter { +class PendingTransactionTracker extends EventEmitter { constructor (config) { super() this.query = new EthQuery(config.provider) @@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this._checkPendingTxs() } - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' + /** + checks if a signed tx is in a block and + if it is included emits tx status as 'confirmed' + @param block {object}, a full block + @emits tx:confirmed + @emits tx:failed + */ checkForTxInBlock (block) { const signedTxList = this.getPendingTransactions() if (!signedTxList.length) return @@ -52,6 +58,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { }) } + /** + asks the network for the transaction to see if a block number is included on it + if we have skipped/missed blocks + @param object - oldBlock newBlock + */ queryPendingTxs ({ oldBlock, newBlock }) { // check pending transactions on start if (!oldBlock) { @@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (diff > 1) this._checkPendingTxs() } - + /** + Will resubmit any transactions who have not been confirmed in a block + @param block {object} - a block object + @emits tx:warning + */ resubmitPendingTxs (block) { const pending = this.getPendingTransactions() // only try resubmitting if their are transactions to resubmit @@ -100,6 +115,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { })) } + /** + resubmits the individual txMeta used in resubmitPendingTxs + @param txMeta {Object} - txMeta object + @param latestBlockNumber {string} - hex string for the latest block number + @emits tx:retry + @returns txHash {string} + */ async _resubmitTx (txMeta, latestBlockNumber) { if (!txMeta.firstRetryBlockNumber) { this.emit('tx:block-update', txMeta, latestBlockNumber) @@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this.emit('tx:retry', txMeta) return txHash } - + /** + Ask the network for the transaction to see if it has been include in a block + @param txMeta {Object} - the txMeta object + @emits tx:failed + @emits tx:confirmed + @emits tx:warning + */ async _checkPendingTx (txMeta) { const txHash = txMeta.hash const txId = txMeta.id @@ -162,8 +190,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter { } } - // checks the network for signed txs and - // if confirmed sets the tx status as 'confirmed' + /** + checks the network for signed txs and releases the nonce global lock if it is + */ async _checkPendingTxs () { const signedTxList = this.getPendingTransactions() // in order to keep the nonceTracker accurate we block it while updating pending transactions @@ -171,12 +200,17 @@ module.exports = class PendingTransactionTracker extends EventEmitter { try { await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) } catch (err) { - console.error('PendingTransactionWatcher - Error updating pending transactions') - console.error(err) + log.error('PendingTransactionWatcher - Error updating pending transactions') + log.error(err) } nonceGlobalLock.releaseLock() } + /** + checks to see if a confirmed txMeta has the same nonce + @param txMeta {Object} - txMeta object + @returns {boolean} + */ async _checkIfNonceIsTaken (txMeta) { const address = txMeta.txParams.from const completed = this.getCompletedTransactions(address) @@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter { }) return sameNonce.length > 0 } - } + +module.exports = PendingTransactionTracker diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index c579e462a..36b5cdbc9 100644 --- a/app/scripts/lib/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -3,22 +3,27 @@ const { hexToBn, BnMultiplyByFraction, bnToHex, -} = require('./util') +} = require('../../lib/util') const { addHexPrefix } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. -/* -tx-utils are utility methods for Transaction manager +/** +tx-gas-utils are gas utility methods for Transaction manager its passed ethquery and used to do things like calculate gas of a tx. +@param {Object} provider - A network provider. */ -module.exports = class TxGasUtil { +class TxGasUtil { constructor (provider) { this.query = new EthQuery(provider) } + /** + @param txMeta {Object} - the txMeta object + @returns {object} the txMeta object with the gas written to the txParams + */ async analyzeGasUsage (txMeta) { const block = await this.query.getBlockByNumber('latest', true) let estimatedGasHex @@ -38,6 +43,12 @@ module.exports = class TxGasUtil { return txMeta } + /** + Estimates the tx's gas usage + @param txMeta {Object} - the txMeta object + @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) { const txParams = txMeta.txParams @@ -70,6 +81,12 @@ module.exports = class TxGasUtil { return await this.query.estimateGas(txParams) } + /** + Writes the gas on the txParams in the txMeta + @param txMeta {Object} - the txMeta object to write to + @param blockGasLimitHex {string} - the block gas limit hex + @param estimatedGasHex {string} - the estimated gas hex + */ setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { txMeta.estimatedGas = addHexPrefix(estimatedGasHex) const txParams = txMeta.txParams @@ -87,6 +104,13 @@ module.exports = class TxGasUtil { return } + /** + Adds a gas buffer with out exceeding the block gas limit + + @param initialGasLimitHex {string} - the initial gas limit to add the buffer too + @param blockGasLimitHex {string} - the block gas limit + @returns {string} the buffered gas limit as a hex string + */ addGasBuffer (initialGasLimitHex, blockGasLimitHex) { const initialGasLimitBn = hexToBn(initialGasLimitHex) const blockGasLimitBn = hexToBn(blockGasLimitHex) @@ -100,4 +124,6 @@ module.exports = class TxGasUtil { // otherwise use blockGasLimit return bnToHex(upperGasLimitBn) } -}
\ No newline at end of file +} + +module.exports = TxGasUtil
\ No newline at end of file diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index c6d10ee62..53428c333 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,22 +1,33 @@ const extend = require('xtend') const EventEmitter = require('events') const ObservableStore = require('obs-store') -const createId = require('./random-id') const ethUtil = require('ethereumjs-util') -const txStateHistoryHelper = require('./tx-state-history-helper') - -// STATUS METHODS - // statuses: - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - // - `'failed'` the tx failed for some reason, included on tx data. - // - `'dropped'` the tx nonce was already used - -module.exports = class TransactionStateManager extends EventEmitter { +const txStateHistoryHelper = require('./lib/tx-state-history-helper') +const createId = require('../../lib/random-id') +const { getFinalStates } = require('./lib/util') +/** + TransactionStateManager is responsible for the state of a transaction and + storing the transaction + it also has some convenience methods for finding subsets of transactions + * + *STATUS METHODS + <br>statuses: + <br> - `'unapproved'` the user has not responded + <br> - `'rejected'` the user has responded no! + <br> - `'approved'` the user has approved the tx + <br> - `'signed'` the tx is signed + <br> - `'submitted'` the tx is sent to a server + <br> - `'confirmed'` the tx has been included in a block. + <br> - `'failed'` the tx failed for some reason, included on tx data. + <br> - `'dropped'` the tx nonce was already used + @param opts {object} + @param {object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array} + @param {number} [opts.txHistoryLimit] limit for how many finished + transactions can hang around in state + @param {function} opts.getNetwork return network number + @class +*/ +class TransactionStateManager extends EventEmitter { constructor ({ initState, txHistoryLimit, getNetwork }) { super() @@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter { this.getNetwork = getNetwork } + /** + @param opts {object} - the object to use when overwriting defaults + @returns {txMeta} the default txMeta object + */ generateTxMeta (opts) { return extend({ id: createId(), @@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter { }, opts) } + /** + @returns {array} of txMetas that have been filtered for only the current network + */ getTxList () { const network = this.getNetwork() const fullTxList = this.getFullTxList() return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) } + /** + @returns {array} of all the txMetas in store + */ getFullTxList () { return this.store.getState().transactions } - // Returns the tx list + /** + @returns {array} the tx list whos status is unapproved + */ getUnapprovedTxList () { const txList = this.getTxsByMetaData('status', 'unapproved') return txList.reduce((result, tx) => { @@ -57,18 +80,37 @@ module.exports = class TransactionStateManager extends EventEmitter { }, {}) } + /** + @param [address] {string} - hex prefixed address to sort the txMetas for [optional] + @returns {array} the tx list whos status is submitted if no address is provide + returns all txMetas who's status is submitted for the current network + */ getPendingTransactions (address) { const opts = { status: 'submitted' } if (address) opts.from = address return this.getFilteredTxList(opts) } + /** + @param [address] {string} - hex prefixed address to sort the txMetas for [optional] + @returns {array} the tx list whos status is confirmed if no address is provide + returns all txMetas who's status is confirmed for the current network + */ getConfirmedTransactions (address) { const opts = { status: 'confirmed' } if (address) opts.from = address return this.getFilteredTxList(opts) } + /** + Adds the txMeta to the list of transactions in the store. + if the list is over txHistoryLimit it will remove a transaction that + is in its final state + it will allso add the key `history` to the txMeta with the snap shot of the original + object + @param txMeta {Object} + @returns {object} the txMeta + */ addTx (txMeta) { this.once(`${txMeta.id}:signed`, function (txId) { this.removeAllListeners(`${txMeta.id}:rejected`) @@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - let index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') + const index = transactions.findIndex((metaTx) => { + return getFinalStates().includes(metaTx.status) + }) if (index !== -1) { transactions.splice(index, 1) } @@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter { this._saveTxList(transactions) return txMeta } - // gets tx by Id and returns it + /** + @param txId {number} + @returns {object} the txMeta who matches the given id if none found + for the network returns undefined + */ getTx (txId) { const txMeta = this.getTxsByMetaData('id', txId)[0] return txMeta } + /** + updates the txMeta in the list and adds a history entry + @param txMeta {Object} - the txMeta to update + @param [note] {string} - a not about the update for history + */ updateTx (txMeta, note) { // validate txParams if (txMeta.txParams) { @@ -134,16 +187,23 @@ module.exports = class TransactionStateManager extends EventEmitter { } - // merges txParams obj onto txData.txParams - // use extend to ensure that all fields are filled + /** + merges txParams obj onto txMeta.txParams + use extend to ensure that all fields are filled + @param txId {number} - the id of the txMeta + @param txParams {object} - the updated txParams + */ updateTxParams (txId, txParams) { const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) this.updateTx(txMeta, `txStateManager#updateTxParams`) } - // validates txParams members by type - validateTxParams(txParams) { + /** + validates txParams members by type + @param txParams {object} - txParams to validate + */ + validateTxParams (txParams) { Object.keys(txParams).forEach((key) => { const value = txParams[key] // validate types @@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter { }) } -/* - Takes an object of fields to search for eg: - let thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - err: undefined, - } - and returns a list of tx with all +/** + @param opts {object} - an object of fields to search for eg:<br> + let <code>thingsToLookFor = {<br> + to: '0x0..',<br> + from: '0x0..',<br> + status: 'signed',<br> + err: undefined,<br> + }<br></code> + @param [initialList=this.getTxList()] + @returns a {array} of txMeta with all options matching - + */ + /* ****************HINT**************** | `err: undefined` is like looking | | for a tx with no err | @@ -190,7 +252,14 @@ module.exports = class TransactionStateManager extends EventEmitter { }) return filteredTxList } + /** + @param key {string} - the key to check + @param value - the value your looking for + @param [txList=this.getTxList()] {array} - the list to search. default is the txList + from txStateManager#getTxList + @returns {array} a list of txMetas who matches the search params + */ getTxsByMetaData (key, value, txList = this.getTxList()) { return txList.filter((txMeta) => { if (txMeta.txParams[key]) { @@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter { // get::set status - // should return the status of the tx. + /** + @param txId {number} - the txMeta Id + @return {string} the status of the tx. + */ getTxStatus (txId) { const txMeta = this.getTx(txId) return txMeta.status } - // should update the status of the tx to 'rejected'. + /** + should update the status of the tx to 'rejected'. + @param txId {number} - the txMeta Id + */ setTxStatusRejected (txId) { this._setTxStatus(txId, 'rejected') } - // should update the status of the tx to 'unapproved'. + /** + should update the status of the tx to 'unapproved'. + @param txId {number} - the txMeta Id + */ setTxStatusUnapproved (txId) { this._setTxStatus(txId, 'unapproved') } - // should update the status of the tx to 'approved'. + /** + should update the status of the tx to 'approved'. + @param txId {number} - the txMeta Id + */ setTxStatusApproved (txId) { this._setTxStatus(txId, 'approved') } - // should update the status of the tx to 'signed'. + /** + should update the status of the tx to 'signed'. + @param txId {number} - the txMeta Id + */ setTxStatusSigned (txId) { this._setTxStatus(txId, 'signed') } - // should update the status of the tx to 'submitted'. - // and add a time stamp for when it was called + /** + should update the status of the tx to 'submitted'. + and add a time stamp for when it was called + @param txId {number} - the txMeta Id + */ setTxStatusSubmitted (txId) { const txMeta = this.getTx(txId) txMeta.submittedTime = (new Date()).getTime() @@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter { this._setTxStatus(txId, 'submitted') } - // should update the status of the tx to 'confirmed'. + /** + should update the status of the tx to 'confirmed'. + @param txId {number} - the txMeta Id + */ setTxStatusConfirmed (txId) { this._setTxStatus(txId, 'confirmed') } - // should update the status dropped + /** + should update the status of the tx to 'dropped'. + @param txId {number} - the txMeta Id + */ setTxStatusDropped (txId) { this._setTxStatus(txId, 'dropped') } + /** + should update the status of the tx to 'failed'. + and put the error on the txMeta + @param txId {number} - the txMeta Id + @param err {erroObject} - error object + */ setTxStatusFailed (txId, err) { const txMeta = this.getTx(txId) txMeta.err = { @@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter { this._setTxStatus(txId, 'failed') } + /** + Removes transaction from the given address for the current network + from the txList + @param address {string} - hex string of the from address on the txParams to remove + */ wipeTransactions (address) { // network only tx const txs = this.getFullTxList() @@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter { // PRIVATE METHODS // - // Should find the tx in the tx list and - // update it. - // should set the status in txData + // STATUS METHODS + // statuses: // - `'unapproved'` the user has not responded // - `'rejected'` the user has responded no! // - `'approved'` the user has approved the tx @@ -283,6 +386,15 @@ module.exports = class TransactionStateManager extends EventEmitter { // - `'submitted'` the tx is sent to a server // - `'confirmed'` the tx has been included in a block. // - `'failed'` the tx failed for some reason, included on tx data. + // - `'dropped'` the tx nonce was already used + + /** + @param txId {number} - the txMeta Id + @param status {string} - the status to set on the txMeta + @emits tx:status-update - passes txId and status + @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta + @emits update:badge + */ _setTxStatus (txId, status) { const txMeta = this.getTx(txId) txMeta.status = status @@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter { this.emit('update:badge') } - // Saves the new/updated txList. + /** + Saves the new/updated txList. + @param transactions {array} - the list of transactions to save + */ // Function is intended only for internal use _saveTxList (transactions) { this.store.updateState({ transactions }) } } + +module.exports = TransactionStateManager diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 144534f43..c49d89288 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -1,6 +1,7 @@ // test and development environment variables const env = process.env.METAMASK_ENV const METAMASK_DEBUG = process.env.METAMASK_DEBUG +const { DEFAULT_NETWORK, MAINNET } = require('./controllers/network/enums') /** * @typedef {Object} FirstTimeState @@ -15,7 +16,7 @@ const initialState = { config: {}, NetworkController: { provider: { - type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet', + type: (METAMASK_DEBUG || env === 'test') ? DEFAULT_NETWORK : MAINNET, }, }, } diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 8c3dd8c71..0f7b3d865 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -16,6 +16,24 @@ function noop () {} class AccountTracker extends EventEmitter { + /** + * This module is responsible for tracking any number of accounts and caching their current balances & transaction + * counts. + * + * It also tracks transaction hashes, and checks their inclusion status on each new block. + * + * @typedef {Object} AccountTracker + * @param {Object} opts Initialize various properties of the class. + * @property {Object} store The stored object containing all accounts to track, as well as the current block's gas limit. + * @property {Object} store.accounts The accounts currently stored in this AccountTracker + * @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block + * @property {Object} _provider A provider needed to create the EthQuery instance used within this AccountTracker. + * @property {EthQuery} _query An EthQuery instance used to access account information from the blockchain + * @property {BlockTracker} _blockTracker A BlockTracker instance. Needed to ensure that accounts and their info updates + * when a new block is created. + * @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block + * + */ constructor (opts = {}) { super() @@ -34,10 +52,17 @@ class AccountTracker extends EventEmitter { this._currentBlockNumber = this._blockTracker.currentBlock } - // - // public - // - + /** + * Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this + * AccountTracker. + * + * Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each + * of these accounts are given an updated balance via EthQuery. + * + * @param {array} address The array of hex addresses for accounts with which this AccountTracker's accounts should be + * in sync + * + */ syncWithAddresses (addresses) { const accounts = this.store.getState().accounts const locals = Object.keys(accounts) @@ -61,6 +86,13 @@ class AccountTracker extends EventEmitter { this._updateAccounts() } + /** + * Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be + * given a balance as long this._currentBlockNumber is defined. + * + * @param {string} address A hex address of a new account to store in this AccountTracker's accounts object + * + */ addAccount (address) { const accounts = this.store.getState().accounts accounts[address] = {} @@ -69,16 +101,27 @@ class AccountTracker extends EventEmitter { this._updateAccount(address) } + /** + * Removes an account from this AccountTracker's accounts object + * + * @param {string} address A hex address of a the account to remove + * + */ removeAccount (address) { const accounts = this.store.getState().accounts delete accounts[address] this.store.updateState({ accounts }) } - // - // private - // - + /** + * Given a block, updates this AccountTracker's currentBlockGasLimit, and then updates each local account's balance + * via EthQuery + * + * @private + * @param {object} block Data about the block that contains the data to update to. + * @fires 'block' The updated state, if all account updates are successful + * + */ _updateForBlock (block) { this._currentBlockNumber = block.number const currentBlockGasLimit = block.gasLimit @@ -93,12 +136,26 @@ class AccountTracker extends EventEmitter { }) } + /** + * Calls this._updateAccount for each account in this.store + * + * @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated + * + */ _updateAccounts (cb = noop) { const accounts = this.store.getState().accounts const addresses = Object.keys(accounts) async.each(addresses, this._updateAccount.bind(this), cb) } + /** + * Updates the current balance of an account. Gets an updated balance via this._getAccount. + * + * @private + * @param {string} address A hex address of a the account to be updated + * @param {Function} cb A callback to call once the account at address is successfully update + * + */ _updateAccount (address, cb = noop) { this._getAccount(address, (err, result) => { if (err) return cb(err) @@ -113,6 +170,14 @@ class AccountTracker extends EventEmitter { }) } + /** + * Gets the current balance of an account via EthQuery. + * + * @private + * @param {string} address A hex address of a the account to query + * @param {Function} cb A callback to call once the account at address is successfully update + * + */ _getAccount (address, cb = noop) { const query = this._query async.parallel({ diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 34b603b96..221746467 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,12 +1,11 @@ const ethUtil = require('ethereumjs-util') const normalize = require('eth-sig-util').normalize -const MetamaskConfig = require('../config.js') - - -const MAINNET_RPC = MetamaskConfig.network.mainnet -const ROPSTEN_RPC = MetamaskConfig.network.ropsten -const KOVAN_RPC = MetamaskConfig.network.kovan -const RINKEBY_RPC = MetamaskConfig.network.rinkeby +const { + MAINNET_RPC_URL, + ROPSTEN_RPC_URL, + KOVAN_RPC_URL, + RINKEBY_RPC_URL, +} = require('../controllers/network/enums') /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -154,19 +153,19 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { switch (provider.type) { case 'mainnet': - return MAINNET_RPC + return MAINNET_RPC_URL case 'ropsten': - return ROPSTEN_RPC + return ROPSTEN_RPC_URL case 'kovan': - return KOVAN_RPC + return KOVAN_RPC_URL case 'rinkeby': - return RINKEBY_RPC + return RINKEBY_RPC_URL default: - return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC + return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL } } diff --git a/app/scripts/lib/extractEthjsErrorMessage.js b/app/scripts/lib/extractEthjsErrorMessage.js index bac541735..0f100756f 100644 --- a/app/scripts/lib/extractEthjsErrorMessage.js +++ b/app/scripts/lib/extractEthjsErrorMessage.js @@ -4,17 +4,18 @@ const errorLabelPrefix = 'Error: ' module.exports = extractEthjsErrorMessage -// -// ethjs-rpc provides overly verbose error messages -// if we detect this type of message, we extract the important part -// Below is an example input and output -// -// Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced -// -// Transaction Failed: replacement transaction underpriced -// - - +/** + * Extracts the important part of an ethjs-rpc error message. If the passed error is not an isEthjsRpcError, the error + * is returned unchanged. + * + * @param {string} errorMessage The error message to parse + * @returns {string} Returns an error message, either the same as was passed, or the ending message portion of an isEthjsRpcError + * + * @example + * // returns 'Transaction Failed: replacement transaction underpriced' + * extractEthjsErrorMessage(`Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced`) + * +*/ function extractEthjsErrorMessage(errorMessage) { const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug) if (isEthjsRpcError) { diff --git a/app/scripts/lib/getObjStructure.js b/app/scripts/lib/getObjStructure.js index 3db389507..52250d3fb 100644 --- a/app/scripts/lib/getObjStructure.js +++ b/app/scripts/lib/getObjStructure.js @@ -14,6 +14,15 @@ module.exports = getObjStructure // } // } +/** + * Creates an object that represents the structure of the given object. It replaces all values with the result of their + * type. + * + * @param {object} obj The object for which a 'structure' will be returned. Usually a plain object and not a class. + * @returns {object} The "mapped" version of a deep clone of the passed object, with each non-object property value + * replaced with the javascript type of that value. + * + */ function getObjStructure(obj) { const structure = clone(obj) return deepMap(structure, (value) => { @@ -21,6 +30,14 @@ function getObjStructure(obj) { }) } +/** + * Modifies all the properties and deeply nested of a passed object. Iterates recursively over all nested objects and + * their properties, and covers the entire depth of the object. At each property value which is not an object is modified. + * + * @param {object} target The object to modify + * @param {Function} visit The modifier to apply to each non-object property value + * @returns {object} The modified object + */ function deepMap(target = {}, visit) { Object.entries(target).forEach(([key, value]) => { if (typeof value === 'object' && value !== null) { diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index f52e048e0..901367f04 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -3,8 +3,37 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const createId = require('./random-id') +/** + * Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for + * an eth_sign call is requested. + * + * @see {@link https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign} + * + * @typedef {Object} Message + * @property {number} id An id to track and identify the message object + * @property {Object} msgParams The parameters to pass to the eth_sign method once the signature request is approved. + * @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. + * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request + * @property {number} time The epoch time at which the this message was created + * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' + * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' with + * always have a 'eth_sign' type. + * + */ module.exports = class MessageManager extends EventEmitter { + + /** + * Controller in charge of managing - storing, adding, removing, updating - Messages. + * + * @typedef {Object} MessageManager + * @param {Object} opts @deprecated + * @property {Object} memStore The observable store where Messages are saved. + * @property {Object} memStore.unapprovedMsgs A collection of all Messages in the 'unapproved' state + * @property {number} memStore.unapprovedMsgCount The count of all Messages in this.memStore.unapprobedMsgs + * @property {array} messages Holds all messages that have been created by this MessageManager + * + */ constructor (opts) { super() this.memStore = new ObservableStore({ @@ -14,15 +43,35 @@ module.exports = class MessageManager extends EventEmitter { this.messages = [] } + /** + * A getter for the number of 'unapproved' Messages in this.messages + * + * @returns {number} The number of 'unapproved' Messages in this.messages + * + */ get unapprovedMsgCount () { return Object.keys(this.getUnapprovedMsgs()).length } + /** + * A getter for the 'unapproved' Messages in this.messages + * + * @returns {Object} An index of Message ids to Messages, for all 'unapproved' Messages in this.messages + * + */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) } + /** + * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the + * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @returns {number} The id of the newly created message. + * + */ addUnapprovedMessage (msgParams) { msgParams.data = normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data @@ -42,24 +91,61 @@ module.exports = class MessageManager extends EventEmitter { return msgId } + /** + * Adds a passed Message to this.messages, and calls this._saveMsgList() to save the unapproved Messages from that + * list to this.memStore. + * + * @param {Message} msg The Message to add to this.messages + * + */ addMsg (msg) { this.messages.push(msg) this._saveMsgList() } + /** + * Returns a specified Message. + * + * @param {number} msgId The id of the Message to get + * @returns {Message|undefined} The Message with the id that matches the passed msgId, or undefined if no Message has that id. + * + */ getMsg (msgId) { return this.messages.find(msg => msg.id === msgId) } + /** + * Approves a Message. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise with + * any the message params modified for proper signing. + * + * @param {Object} msgParams The msgParams to be used when eth_sign is called, plus data added by MetaMask. + * @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. + * @returns {Promise<object>} Promises the msgParams object with metamaskId removed. + * + */ approveMessage (msgParams) { this.setMsgStatusApproved(msgParams.metamaskId) return this.prepMsgForSigning(msgParams) } + /** + * Sets a Message status to 'approved' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the Message to approve. + * + */ setMsgStatusApproved (msgId) { this._setMsgStatus(msgId, 'approved') } + /** + * Sets a Message status to 'signed' via a call to this._setMsgStatus and updates that Message in this.messages by + * adding the raw signature data of the signature request to the Message + * + * @param {number} msgId The id of the Message to sign. + * @param {buffer} rawSig The raw data of the signature request + * + */ setMsgStatusSigned (msgId, rawSig) { const msg = this.getMsg(msgId) msg.rawSig = rawSig @@ -67,19 +153,40 @@ module.exports = class MessageManager extends EventEmitter { this._setMsgStatus(msgId, 'signed') } + /** + * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams + * + * @param {Object} msgParams The msgParams to modify + * @returns {Promise<object>} Promises the msgParams with the metamaskId property removed + * + */ prepMsgForSigning (msgParams) { delete msgParams.metamaskId return Promise.resolve(msgParams) } + /** + * Sets a Message status to 'rejected' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the Message to reject. + * + */ rejectMsg (msgId) { this._setMsgStatus(msgId, 'rejected') } - // - // PRIVATE METHODS - // - + /** + * Updates the status of a Message in this.messages via a call to this._updateMsg + * + * @private + * @param {number} msgId The id of the Message to update. + * @param {string} status The new status of the Message. + * @throws A 'MessageManager - Message not found for id: "${msgId}".' if there is no Message in this.messages with an + * id equal to the passed msgId + * @fires An event with a name equal to `${msgId}:${status}`. The Message is also fired. + * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along with the message + * + */ _setMsgStatus (msgId, status) { const msg = this.getMsg(msgId) if (!msg) throw new Error('MessageManager - Message not found for id: "${msgId}".') @@ -91,6 +198,14 @@ module.exports = class MessageManager extends EventEmitter { } } + /** + * Sets a Message in this.messages to the passed Message if the ids are equal. Then saves the unapprovedMsg list to + * storage via this._saveMsgList + * + * @private + * @param {msg} Message A Message that will replace an existing Message (with the same id) in this.messages + * + */ _updateMsg (msg) { const index = this.messages.findIndex((message) => message.id === msg.id) if (index !== -1) { @@ -99,6 +214,13 @@ module.exports = class MessageManager extends EventEmitter { this._saveMsgList() } + /** + * Saves the unapproved messages, and their count, to this.memStore + * + * @private + * @fires 'updateBadge' + * + */ _saveMsgList () { const unapprovedMsgs = this.getUnapprovedMsgs() const unapprovedMsgCount = Object.keys(unapprovedMsgs).length @@ -108,6 +230,13 @@ module.exports = class MessageManager extends EventEmitter { } +/** + * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. + * + * @param {any} data The buffer data to convert to a hex + * @returns {string} A hex string conversion of the buffer data + * + */ function normalizeMsgData (data) { if (data.slice(0, 2) === '0x') { // data is already hex diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 1fcb7cf69..5dfb42078 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -5,10 +5,18 @@ const width = 360 class NotificationManager { - // - // Public - // + /** + * A collection of methods for controlling the showing and hiding of the notification popup. + * + * @typedef {Object} NotificationManager + * + */ + /** + * Either brings an existing MetaMask notification window into focus, or creates a new notification window. New + * notification windows are given a 'popup' type. + * + */ showPopup () { this._getPopup((err, popup) => { if (err) throw err @@ -29,6 +37,10 @@ class NotificationManager { }) } + /** + * Closes a MetaMask notification if it window exists. + * + */ closePopup () { // closes notification popup this._getPopup((err, popup) => { @@ -38,10 +50,14 @@ class NotificationManager { }) } - // - // Private - // - + /** + * Checks all open MetaMask windows, and returns the first one it finds that is a notification window (i.e. has the + * type 'popup') + * + * @private + * @param {Function} cb A node style callback that to whcih the found notification window will be passed. + * + */ _getPopup (cb) { this._getWindows((err, windows) => { if (err) throw err @@ -49,6 +65,13 @@ class NotificationManager { }) } + /** + * Returns all open MetaMask windows. + * + * @private + * @param {Function} cb A node style callback that to which the windows will be passed. + * + */ _getWindows (cb) { // Ignore in test environment if (!extension.windows) { @@ -60,6 +83,13 @@ class NotificationManager { }) } + /** + * Given an array of windows, returns the first that has a 'popup' type, or null if no such window exists. + * + * @private + * @param {array} windows An array of objects containing data about the open MetaMask extension windows. + * + */ _getPopupIn (windows) { return windows ? windows.find((win) => { // Returns notification popup diff --git a/app/scripts/lib/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js index 6ae526463..0f1dc19a9 100644 --- a/app/scripts/lib/pending-balance-calculator.js +++ b/app/scripts/lib/pending-balance-calculator.js @@ -3,16 +3,28 @@ const normalize = require('eth-sig-util').normalize class PendingBalanceCalculator { - // Must be initialized with two functions: - // getBalance => Returns a promise of a BN of the current balance in Wei - // getPendingTransactions => Returns an array of TxMeta Objects, - // which have txParams properties, which include value, gasPrice, and gas, - // all in a base=16 hex format. + /** + * Used for calculating a users "pending balance": their current balance minus the total possible cost of all their + * pending transactions. + * + * @typedef {Object} PendingBalanceCalculator + * @param {Function} getBalance Returns a promise of a BN of the current balance in Wei + * @param {Function} getPendingTransactions Returns an array of TxMeta Objects, which have txParams properties, + * which include value, gasPrice, and gas, all in a base=16 hex format. + * + */ constructor ({ getBalance, getPendingTransactions }) { this.getPendingTransactions = getPendingTransactions this.getNetworkBalance = getBalance } + /** + * Returns the users "pending balance": their current balance minus the total possible cost of all their + * pending transactions. + * + * @returns {Promise<string>} Promises a base 16 hex string that contains the user's "pending balance" + * + */ async getBalance () { const results = await Promise.all([ this.getNetworkBalance(), @@ -29,6 +41,15 @@ class PendingBalanceCalculator { return `0x${balance.sub(pendingValue).toString(16)}` } + /** + * Calculates the maximum possible cost of a single transaction, based on the value, gas price and gas limit. + * + * @param {object} tx Contains all that data about a transaction. + * @property {object} tx.txParams Contains data needed to calculate the maximum cost of the transaction: gas, + * gasLimit and value. + * + * @returns {string} Returns a base 16 hex string that contains the maximum possible cost of the transaction. + */ calculateMaxCost (tx) { const txValue = tx.txParams.value const value = this.hexToBn(txValue) @@ -42,6 +63,13 @@ class PendingBalanceCalculator { return value.add(gasCost) } + /** + * Converts a hex string to a BN object + * + * @param {string} hex A number represented as a hex string + * @returns {Object} A BN object + * + */ hexToBn (hex) { return new BN(normalize(hex).substring(2), 16) } diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 43a7d0b42..e96ced1f2 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -5,8 +5,37 @@ const createId = require('./random-id') const hexRe = /^[0-9A-Fa-f]+$/g const log = require('loglevel') +/** + * Represents, and contains data about, an 'personal_sign' type signature request. These are created when a + * signature for an personal_sign call is requested. + * + * @see {@link https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html#sign} + * + * @typedef {Object} PersonalMessage + * @property {number} id An id to track and identify the message object + * @property {Object} msgParams The parameters to pass to the personal_sign method once the signature request is + * approved. + * @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. + * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request + * @property {number} time The epoch time at which the this message was created + * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' + * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will + * always have a 'personal_sign' type. + * + */ module.exports = class PersonalMessageManager extends EventEmitter { + /** + * Controller in charge of managing - storing, adding, removing, updating - PersonalMessage. + * + * @typedef {Object} PersonalMessageManager + * @param {Object} opts @deprecated + * @property {Object} memStore The observable store where PersonalMessage are saved with persistance. + * @property {Object} memStore.unapprovedPersonalMsgs A collection of all PersonalMessages in the 'unapproved' state + * @property {number} memStore.unapprovedPersonalMsgCount The count of all PersonalMessages in this.memStore.unapprobedMsgs + * @property {array} messages Holds all messages that have been created by this PersonalMessageManager + * + */ constructor (opts) { super() this.memStore = new ObservableStore({ @@ -16,15 +45,37 @@ module.exports = class PersonalMessageManager extends EventEmitter { this.messages = [] } + /** + * A getter for the number of 'unapproved' PersonalMessages in this.messages + * + * @returns {number} The number of 'unapproved' PersonalMessages in this.messages + * + */ get unapprovedPersonalMsgCount () { return Object.keys(this.getUnapprovedMsgs()).length } + /** + * A getter for the 'unapproved' PersonalMessages in this.messages + * + * @returns {Object} An index of PersonalMessage ids to PersonalMessages, for all 'unapproved' PersonalMessages in + * this.messages + * + */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) } + /** + * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to + * this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @returns {number} The id of the newly created PersonalMessage. + * + */ addUnapprovedMessage (msgParams) { log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) msgParams.data = this.normalizeMsgData(msgParams.data) @@ -45,24 +96,62 @@ module.exports = class PersonalMessageManager extends EventEmitter { return msgId } + /** + * Adds a passed PersonalMessage to this.messages, and calls this._saveMsgList() to save the unapproved PersonalMessages from that + * list to this.memStore. + * + * @param {Message} msg The PersonalMessage to add to this.messages + * + */ addMsg (msg) { this.messages.push(msg) this._saveMsgList() } + /** + * Returns a specified PersonalMessage. + * + * @param {number} msgId The id of the PersonalMessage to get + * @returns {PersonalMessage|undefined} The PersonalMessage with the id that matches the passed msgId, or undefined + * if no PersonalMessage has that id. + * + */ getMsg (msgId) { return this.messages.find(msg => msg.id === msgId) } + /** + * Approves a PersonalMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise + * with any the message params modified for proper signing. + * + * @param {Object} msgParams The msgParams to be used when eth_sign is called, plus data added by MetaMask. + * @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. + * @returns {Promise<object>} Promises the msgParams object with metamaskId removed. + * + */ approveMessage (msgParams) { this.setMsgStatusApproved(msgParams.metamaskId) return this.prepMsgForSigning(msgParams) } + /** + * Sets a PersonalMessage status to 'approved' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the PersonalMessage to approve. + * + */ setMsgStatusApproved (msgId) { this._setMsgStatus(msgId, 'approved') } + /** + * Sets a PersonalMessage status to 'signed' via a call to this._setMsgStatus and updates that PersonalMessage in + * this.messages by adding the raw signature data of the signature request to the PersonalMessage + * + * @param {number} msgId The id of the PersonalMessage to sign. + * @param {buffer} rawSig The raw data of the signature request + * + */ setMsgStatusSigned (msgId, rawSig) { const msg = this.getMsg(msgId) msg.rawSig = rawSig @@ -70,19 +159,41 @@ module.exports = class PersonalMessageManager extends EventEmitter { this._setMsgStatus(msgId, 'signed') } + /** + * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams + * + * @param {Object} msgParams The msgParams to modify + * @returns {Promise<object>} Promises the msgParams with the metamaskId property removed + * + */ prepMsgForSigning (msgParams) { delete msgParams.metamaskId return Promise.resolve(msgParams) } + /** + * Sets a PersonalMessage status to 'rejected' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the PersonalMessage to reject. + * + */ rejectMsg (msgId) { this._setMsgStatus(msgId, 'rejected') } - // - // PRIVATE METHODS - // - + /** + * Updates the status of a PersonalMessage in this.messages via a call to this._updateMsg + * + * @private + * @param {number} msgId The id of the PersonalMessage to update. + * @param {string} status The new status of the PersonalMessage. + * @throws A 'PersonalMessageManager - PersonalMessage not found for id: "${msgId}".' if there is no PersonalMessage + * in this.messages with an id equal to the passed msgId + * @fires An event with a name equal to `${msgId}:${status}`. The PersonalMessage is also fired. + * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along + * with the PersonalMessage + * + */ _setMsgStatus (msgId, status) { const msg = this.getMsg(msgId) if (!msg) throw new Error('PersonalMessageManager - Message not found for id: "${msgId}".') @@ -94,6 +205,15 @@ module.exports = class PersonalMessageManager extends EventEmitter { } } + /** + * Sets a PersonalMessage in this.messages to the passed PersonalMessage if the ids are equal. Then saves the + * unapprovedPersonalMsgs index to storage via this._saveMsgList + * + * @private + * @param {msg} PersonalMessage A PersonalMessage that will replace an existing PersonalMessage (with the same + * id) in this.messages + * + */ _updateMsg (msg) { const index = this.messages.findIndex((message) => message.id === msg.id) if (index !== -1) { @@ -102,6 +222,13 @@ module.exports = class PersonalMessageManager extends EventEmitter { this._saveMsgList() } + /** + * Saves the unapproved PersonalMessages, and their count, to this.memStore + * + * @private + * @fires 'updateBadge' + * + */ _saveMsgList () { const unapprovedPersonalMsgs = this.getUnapprovedMsgs() const unapprovedPersonalMsgCount = Object.keys(unapprovedPersonalMsgs).length @@ -109,6 +236,13 @@ module.exports = class PersonalMessageManager extends EventEmitter { this.emit('updateBadge') } + /** + * A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex. + * + * @param {any} data The buffer data to convert to a hex + * @returns {string} A hex string conversion of the buffer data + * + */ normalizeMsgData (data) { try { const stripped = ethUtil.stripHexPrefix(data) diff --git a/app/scripts/lib/seed-phrase-verifier.js b/app/scripts/lib/seed-phrase-verifier.js index 7ba712c0d..3b5afb800 100644 --- a/app/scripts/lib/seed-phrase-verifier.js +++ b/app/scripts/lib/seed-phrase-verifier.js @@ -3,11 +3,19 @@ const log = require('loglevel') const seedPhraseVerifier = { - // Verifies if the seed words can restore the accounts. - // - // The seed words can recreate the primary keyring and the accounts belonging to it. - // The created accounts in the primary keyring are always the same. - // The keyring always creates the accounts in the same sequence. + /** + * Verifies if the seed words can restore the accounts. + * + * Key notes: + * - The seed words can recreate the primary keyring and the accounts belonging to it. + * - The created accounts in the primary keyring are always the same. + * - The keyring always creates the accounts in the same sequence. + * + * @param {array} createdAccounts The accounts to restore + * @param {string} seedWords The seed words to verify + * @returns {Promise<void>} Promises undefined + * + */ verifyAccounts (createdAccounts, seedWords) { return new Promise((resolve, reject) => { diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 9ec9a256f..48b941c3d 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -24,19 +24,20 @@ function setupRaven(opts) { transport: function(opts) { const report = opts.data // simplify certain complex error messages - report.exception.values.forEach(item => { - let errorMessage = item.value - // simplify ethjs error messages - errorMessage = extractEthjsErrorMessage(errorMessage) - // simplify 'Transaction Failed: known transaction' - if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) { - // cut the hash from the error message - errorMessage = 'Transaction Failed: known transaction' - } - // finalize - item.value = errorMessage - }) - + if (report.exception && report.exception.values) { + report.exception.values.forEach(item => { + let errorMessage = item.value + // simplify ethjs error messages + errorMessage = extractEthjsErrorMessage(errorMessage) + // simplify 'Transaction Failed: known transaction' + if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) { + // cut the hash from the error message + errorMessage = 'Transaction Failed: known transaction' + } + // finalize + item.value = errorMessage + }) + } // modify report urls rewriteReportUrls(report) // make request normally @@ -52,11 +53,13 @@ function rewriteReportUrls(report) { // update request url report.request.url = toMetamaskUrl(report.request.url) // update exception stack trace - report.exception.values.forEach(item => { - item.stacktrace.frames.forEach(frame => { - frame.filename = toMetamaskUrl(frame.filename) + if (report.exception && report.exception.values) { + report.exception.values.forEach(item => { + item.stacktrace.frames.forEach(frame => { + frame.filename = toMetamaskUrl(frame.filename) + }) }) - }) + } } function toMetamaskUrl(origUrl) { diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index 60042155e..c58921610 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -5,7 +5,36 @@ const assert = require('assert') const sigUtil = require('eth-sig-util') const log = require('loglevel') +/** + * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a + * signature for an eth_signTypedData call is requested. + * + * @typedef {Object} TypedMessage + * @property {number} id An id to track and identify the message object + * @property {Object} msgParams The parameters to pass to the eth_signTypedData method once the signature request is + * approved. + * @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. + * @property {Object} msgParams.from The address that is making the signature request. + * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request + * @property {number} time The epoch time at which the this message was created + * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' + * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will + * always have a 'eth_signTypedData' type. + * + */ + module.exports = class TypedMessageManager extends EventEmitter { + /** + * Controller in charge of managing - storing, adding, removing, updating - TypedMessage. + * + * @typedef {Object} TypedMessage + * @param {Object} opts @deprecated + * @property {Object} memStore The observable store where TypedMessage are saved. + * @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state + * @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs + * @property {array} messages Holds all messages that have been created by this TypedMessage + * + */ constructor (opts) { super() this.memStore = new ObservableStore({ @@ -15,15 +44,37 @@ module.exports = class TypedMessageManager extends EventEmitter { this.messages = [] } + /** + * A getter for the number of 'unapproved' TypedMessages in this.messages + * + * @returns {number} The number of 'unapproved' TypedMessages in this.messages + * + */ get unapprovedTypedMessagesCount () { return Object.keys(this.getUnapprovedMsgs()).length } + /** + * A getter for the 'unapproved' TypedMessages in this.messages + * + * @returns {Object} An index of TypedMessage ids to TypedMessages, for all 'unapproved' TypedMessages in + * this.messages + * + */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) } + /** + * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to + * this.memStore. Before any of this is done, msgParams are validated + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @returns {number} The id of the newly created TypedMessage. + * + */ addUnapprovedMessage (msgParams) { this.validateParams(msgParams) @@ -45,6 +96,12 @@ module.exports = class TypedMessageManager extends EventEmitter { return msgId } + /** + * Helper method for this.addUnapprovedMessage. Validates that the passed params have the required properties. + * + * @param {Object} params The params to validate + * + */ validateParams (params) { assert.equal(typeof params, 'object', 'Params should ben an object.') assert.ok('data' in params, 'Params must include a data field.') @@ -56,24 +113,62 @@ module.exports = class TypedMessageManager extends EventEmitter { }, 'Expected EIP712 typed data') } + /** + * Adds a passed TypedMessage to this.messages, and calls this._saveMsgList() to save the unapproved TypedMessages from that + * list to this.memStore. + * + * @param {Message} msg The TypedMessage to add to this.messages + * + */ addMsg (msg) { this.messages.push(msg) this._saveMsgList() } + /** + * Returns a specified TypedMessage. + * + * @param {number} msgId The id of the TypedMessage to get + * @returns {TypedMessage|undefined} The TypedMessage with the id that matches the passed msgId, or undefined + * if no TypedMessage has that id. + * + */ getMsg (msgId) { return this.messages.find(msg => msg.id === msgId) } + /** + * Approves a TypedMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise + * with any the message params modified for proper signing. + * + * @param {Object} msgParams The msgParams to be used when eth_sign is called, plus data added by MetaMask. + * @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask. + * @returns {Promise<object>} Promises the msgParams object with metamaskId removed. + * + */ approveMessage (msgParams) { this.setMsgStatusApproved(msgParams.metamaskId) return this.prepMsgForSigning(msgParams) } + /** + * Sets a TypedMessage status to 'approved' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the TypedMessage to approve. + * + */ setMsgStatusApproved (msgId) { this._setMsgStatus(msgId, 'approved') } + /** + * Sets a TypedMessage status to 'signed' via a call to this._setMsgStatus and updates that TypedMessage in + * this.messages by adding the raw signature data of the signature request to the TypedMessage + * + * @param {number} msgId The id of the TypedMessage to sign. + * @param {buffer} rawSig The raw data of the signature request + * + */ setMsgStatusSigned (msgId, rawSig) { const msg = this.getMsg(msgId) msg.rawSig = rawSig @@ -81,11 +176,24 @@ module.exports = class TypedMessageManager extends EventEmitter { this._setMsgStatus(msgId, 'signed') } + /** + * Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams + * + * @param {Object} msgParams The msgParams to modify + * @returns {Promise<object>} Promises the msgParams with the metamaskId property removed + * + */ prepMsgForSigning (msgParams) { delete msgParams.metamaskId return Promise.resolve(msgParams) } + /** + * Sets a TypedMessage status to 'rejected' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the TypedMessage to reject. + * + */ rejectMsg (msgId) { this._setMsgStatus(msgId, 'rejected') } @@ -94,6 +202,19 @@ module.exports = class TypedMessageManager extends EventEmitter { // PRIVATE METHODS // + /** + * Updates the status of a TypedMessage in this.messages via a call to this._updateMsg + * + * @private + * @param {number} msgId The id of the TypedMessage to update. + * @param {string} status The new status of the TypedMessage. + * @throws A 'TypedMessageManager - TypedMessage not found for id: "${msgId}".' if there is no TypedMessage + * in this.messages with an id equal to the passed msgId + * @fires An event with a name equal to `${msgId}:${status}`. The TypedMessage is also fired. + * @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along + * with the TypedMessage + * + */ _setMsgStatus (msgId, status) { const msg = this.getMsg(msgId) if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".') @@ -105,6 +226,15 @@ module.exports = class TypedMessageManager extends EventEmitter { } } + /** + * Sets a TypedMessage in this.messages to the passed TypedMessage if the ids are equal. Then saves the + * unapprovedTypedMsgs index to storage via this._saveMsgList + * + * @private + * @param {msg} TypedMessage A TypedMessage that will replace an existing TypedMessage (with the same + * id) in this.messages + * + */ _updateMsg (msg) { const index = this.messages.findIndex((message) => message.id === msg.id) if (index !== -1) { @@ -113,6 +243,13 @@ module.exports = class TypedMessageManager extends EventEmitter { this._saveMsgList() } + /** + * Saves the unapproved TypedMessages, and their count, to this.memStore + * + * @private + * @fires 'updateBadge' + * + */ _saveMsgList () { const unapprovedTypedMessages = this.getUnapprovedMsgs() const unapprovedTypedMessagesCount = Object.keys(unapprovedTypedMessages).length diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a12b6776e..c4a73d8ea 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -263,6 +263,7 @@ 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 @@ -313,9 +314,11 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Returns an api-object which is consumed by the UI + * Returns an Object containing API Callback Functions. + * These functions are the interface for the UI. + * The API object can be transmitted over a stream with dnode. * - * @returns {Object} + * @returns {Object} Object containing API functions. */ getApi () { const keyringController = this.keyringController @@ -405,16 +408,18 @@ module.exports = class MetamaskController extends EventEmitter { //============================================================================= /** - * Creates a new Vault(?) and create a new keychain(?) + * Creates a new Vault and create a new keychain. * - * A vault is ... + * A vault, or KeyringController, is a controller that contains + * many different account strategies, currently called Keyrings. + * Creating it new means wiping all previous keyrings. * - * A keychain is ... + * A keychain, or keyring, controls many accounts with a single backup and signing strategy. + * For example, a mnemonic phrase can generate many accounts, and is a keyring. * + * @param {string} password * - * @param {} password - * - * @returns {} vault + * @returns {Object} vault */ async createNewVaultAndKeychain (password) { const release = await this.createVaultMutex.acquire() @@ -440,7 +445,7 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Create a new Vault and restore an existent keychain + * Create a new Vault and restore an existent keyring. * @param {} password * @param {} seed */ @@ -458,10 +463,16 @@ module.exports = class MetamaskController extends EventEmitter { } /** + * @type Identity + * @property {string} name - The account nickname. + * @property {string} address - The account's ethereum address, in lower case. + * @property {boolean} mayBeFauceting - Whether this account is currently + * receiving funds from our automatic Ropsten faucet. + */ + + /** * Retrieves the first Identiy from the passed Vault and selects the related address * - * An Identity is ... - * * @param {} vault */ selectFirstIdentity (vault) { @@ -470,12 +481,12 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController.setSelectedAddress(address) } - // ? - // Opinionated Keyring Management + // + // Account Management // /** - * Adds a new account to ... + * Adds a new account to the default (first) HD seed phrase Keyring. * * @returns {} keyState */ @@ -505,6 +516,8 @@ module.exports = class MetamaskController extends EventEmitter { * * Used when creating a first vault, to allow confirmation. * Also used when revealing the seed words in the confirmation view. + * + * @param {Function} cb - A callback called on completion. */ placeSeedWords (cb) { @@ -524,6 +537,8 @@ module.exports = class MetamaskController extends EventEmitter { * Validity: seed phrase restores the accounts belonging to the current vault. * * Called when the first account is created and on unlocking the vault. + * + * @returns {Promise<string>} Seed phrase to be confirmed by the user. */ async verifySeedPhrase () { @@ -554,6 +569,7 @@ module.exports = class MetamaskController extends EventEmitter { * * The seed phrase remains available in the background process. * + * @param {function} cb Callback function called with the current address. */ clearSeedWordCache (cb) { this.configManager.setSeedWords(null) @@ -561,9 +577,13 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * ? + * Clears the transaction history, to allow users to force-reset their nonces. + * Mostly used in development environments, when networks are restarted with + * the same network ID. + * + * @returns Promise<string> The current selected address. */ - async resetAccount (cb) { + async resetAccount () { const selectedAddress = this.preferencesController.getSelectedAddress() this.txController.wipeTransactions(selectedAddress) @@ -575,11 +595,13 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Imports an account ... ? + * Imports an account with the specified import strategy. + * These are defined in app/scripts/account-import-strategies + * Each strategy represents a different way of serializing an Ethereum key pair. * - * @param {} strategy - * @param {} args - * @param {} cb + * @param {string} strategy - A unique identifier for an account import strategy. + * @param {any} args - The data required by that strategy to import an account. + * @param {Function} cb - A callback function called with a state update on success. */ importAccountWithStrategy (strategy, args, cb) { accountImporter.importAccount(strategy, args) @@ -593,13 +615,42 @@ module.exports = class MetamaskController extends EventEmitter { } // --------------------------------------------------------------------------- - // Identity Management (sign) + // Identity Management (signature operations) + + // eth_sign methods: + + /** + * Called when a Dapp uses the eth_sign method, to request user approval. + * eth_sign is a pure signature of arbitrary data. It is on a deprecation + * path, since this data can be a transaction, or can leak private key + * information. + * + * @param {Object} msgParams - The params passed to eth_sign. + * @param {Function} cb = The callback function called with the signature. + */ + newUnsignedMessage (msgParams, cb) { + const msgId = this.messageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.messageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } /** - * @param {} msgParams - * @param {} cb + * Signifies user intent to complete an eth_sign method. + * + * @param {Object} msgParams The params passed to eth_call. + * @returns {Promise<Object>} Full state update. */ - signMessage (msgParams, cb) { + signMessage (msgParams) { log.info('MetaMaskController - signMessage') const msgId = msgParams.metamaskId @@ -618,14 +669,37 @@ module.exports = class MetamaskController extends EventEmitter { }) } - // Prefixed Style Message Signing Methods: + /** + * Used to cancel a message submitted via eth_sign. + * + * @param {string} msgId - The id of the message to cancel. + */ + cancelMessage (msgId, cb) { + const messageManager = this.messageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + + // personal_sign methods: /** + * Called when a dapp uses the personal_sign method. + * This is identical to the Geth eth_sign method, and may eventually replace + * eth_sign. * - * @param {} msgParams - * @param {} cb + * We currently define our eth_sign and personal_sign mostly for legacy Dapps. + * + * @param {Object} msgParams - The params of the message to sign & return to the Dapp. + * @param {Function} cb - The callback function called with the signature. + * Passed back to the requesting Dapp. */ - approvePersonalMessage (msgParams, cb) { + newUnsignedPersonalMessage (msgParams, cb) { + if (!msgParams.from) { + return cb(new Error('MetaMask Message Signature: from field is required.')) + } + const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) this.sendUpdate() this.opts.showUnconfirmedMessage() @@ -634,7 +708,7 @@ module.exports = class MetamaskController extends EventEmitter { case 'signed': return cb(null, data.rawSig) case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied transaction signature.')) + return cb(new Error('MetaMask Message Signature: User denied message signature.')) default: return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) } @@ -642,7 +716,11 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * @param {} msgParams + * Signifies a user's approval to sign a personal_sign message in queue. + * Triggers signing, and the callback function from newUnsignedPersonalMessage. + * + * @param {Object} msgParams - The params of the message to sign & return to the Dapp. + * @returns {Promise<Object>} - A full state update. */ signPersonalMessage (msgParams) { log.info('MetaMaskController - signPersonalMessage') @@ -663,7 +741,54 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * @param {} msgParams + * Used to cancel a personal_sign type message. + * @param {string} msgId - The ID of the message to cancel. + * @param {Function} cb - The callback function called with a full state update. + */ + cancelPersonalMessage (msgId, cb) { + const messageManager = this.personalMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + + // eth_signTypedData methods + + /** + * Called when a dapp uses the eth_signTypedData method, per EIP 712. + * + * @param {Object} msgParams - The params passed to eth_signTypedData. + * @param {Function} cb - The callback function, called with the signature. + */ + newUnsignedTypedMessage (msgParams, cb) { + let msgId + try { + msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + } catch (e) { + return cb(e) + } + + this.typedMessageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + /** + * The method for a user approving a call to eth_signTypedData, per EIP 712. + * Triggers the callback in newUnsignedTypedMessage. + * + * @param {Object} msgParams - The params passed to eth_signTypedData. + * @returns {Object} Full state update. */ signTypedMessage (msgParams) { log.info('MetaMaskController - signTypedMessage') @@ -683,12 +808,30 @@ module.exports = class MetamaskController extends EventEmitter { }) } + /** + * Used to cancel a eth_signTypedData type message. + * @param {string} msgId - The ID of the message to cancel. + * @param {Function} cb - The callback function called with a full state update. + */ + cancelTypedMessage (msgId, cb) { + const messageManager = this.typedMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + // --------------------------------------------------------------------------- - // Account Restauration + // MetaMask Version 3 Migration Account Restauration Methods /** - * ? + * A legacy method (probably dead code) that was used when we swapped out our + * key management library that we depended on. * + * Described in: + * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd + * + * @deprecated * @param {} migratorOutput */ restoreOldVaultAccounts (migratorOutput) { @@ -698,8 +841,26 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * ? + * A legacy method used to record user confirmation that they understand + * that some of their accounts have been recovered but should be backed up. + * + * @deprecated + * @param {Function} cb - A callback function called with a full state update. + */ + markAccountsFound (cb) { + this.configManager.setLostAccounts([]) + this.sendUpdate() + cb(null, this.getState()) + } + + /** + * A legacy method (probably dead code) that was used when we swapped out our + * key management library that we depended on. * + * Described in: + * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd + * + * @deprecated * @param {} migratorOutput */ restoreOldLostAccounts (migratorOutput) { @@ -712,12 +873,23 @@ module.exports = class MetamaskController extends EventEmitter { } /** - * Import (lost) Accounts + * An account object + * @typedef Account + * @property string privateKey - The private key of the account. + */ + + /** + * Probably no longer needed, related to the Version 3 migration. + * Imports a hash of accounts to private keys into the vault. * - * @param {Object} {lostAccounts} @Array accounts <{ address, privateKey }> + * Described in: + * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd * * Uses the array's private keys to create a new Simple Key Pair keychain * and add it to the keyring controller. + * @deprecated + * @param {Account[]} lostAccounts - + * @returns {Keyring[]} An array of the restored keyrings. */ importLostAccounts ({ lostAccounts }) { const privKeys = lostAccounts.map(acct => acct.privateKey) @@ -731,113 +903,37 @@ module.exports = class MetamaskController extends EventEmitter { // END (VAULT / KEYRING RELATED METHODS) //============================================================================= -// - -//============================================================================= -// MESSAGES -//============================================================================= - + /** + * Allows a user to try to speed up a transaction by retrying it + * 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, cb) { await this.txController.retryTransaction(txId) const state = await this.getState() return state } +//============================================================================= +// PASSWORD MANAGEMENT +//============================================================================= - newUnsignedMessage (msgParams, cb) { - const msgId = this.messageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - this.messageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - newUnsignedPersonalMessage (msgParams, cb) { - if (!msgParams.from) { - return cb(new Error('MetaMask Message Signature: from field is required.')) - } - - const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - this.personalMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - newUnsignedTypedMessage (msgParams, cb) { - let msgId - try { - msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - } catch (e) { - return cb(e) - } - - this.typedMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - cancelMessage (msgId, cb) { - const messageManager = this.messageManager - messageManager.rejectMsg(msgId) - if (cb && typeof cb === 'function') { - cb(null, this.getState()) - } - } - - cancelPersonalMessage (msgId, cb) { - const messageManager = this.personalMessageManager - messageManager.rejectMsg(msgId) - if (cb && typeof cb === 'function') { - cb(null, this.getState()) - } - } - - cancelTypedMessage (msgId, cb) { - const messageManager = this.typedMessageManager - messageManager.rejectMsg(msgId) - if (cb && typeof cb === 'function') { - cb(null, this.getState()) - } - } - - markAccountsFound (cb) { - this.configManager.setLostAccounts([]) - this.sendUpdate() - cb(null, this.getState()) - } - + /** + * Allows a user to begin the seed phrase recovery process. + * @param {Function} cb - A callback function called when complete. + */ markPasswordForgotten(cb) { this.configManager.setPasswordForgotten(true) this.sendUpdate() cb() } + /** + * Allows a user to end the seed phrase recovery process. + * @param {Function} cb - A callback function called when complete. + */ unMarkPasswordForgotten(cb) { this.configManager.setPasswordForgotten(false) this.sendUpdate() @@ -848,6 +944,13 @@ module.exports = class MetamaskController extends EventEmitter { // SETUP //============================================================================= + /** + * Used to create a multiplexed stream for connecting to an untrusted context + * like a Dapp or other extension. + * @param {*} connectionStream - The Duplex stream to connect to. + * @param {string} originDomain - The domain requesting the stream, which + * may trigger a blacklist reload. + */ setupUntrustedCommunication (connectionStream, originDomain) { // Check if new connection is blacklisted if (this.blacklistController.checkForPhishing(originDomain)) { @@ -863,6 +966,16 @@ module.exports = class MetamaskController extends EventEmitter { this.setupPublicConfig(mux.createStream('publicConfig')) } + /** + * Used to create a multiplexed stream for connecting to a trusted context, + * like our own user interfaces, which have the provider APIs, but also + * receive the exported API from this controller, which includes trusted + * functions, like the ability to approve transactions or sign messages. + * + * @param {*} connectionStream - The duplex stream to connect to. + * @param {string} originDomain - The domain requesting the connection, + * used in logging and error reporting. + */ setupTrustedCommunication (connectionStream, originDomain) { // setup multiplexing const mux = setupMultiplex(connectionStream) @@ -871,12 +984,25 @@ module.exports = class MetamaskController extends EventEmitter { this.setupProviderConnection(mux.createStream('provider'), originDomain) } + /** + * Called when we detect a suspicious domain. Requests the browser redirects + * to our anti-phishing page. + * + * @private + * @param {*} connectionStream - The duplex stream to the per-page script, + * for sending the reload attempt to. + * @param {string} hostname - The URL that triggered the suspicion. + */ sendPhishingWarning (connectionStream, hostname) { const mux = setupMultiplex(connectionStream) const phishingStream = mux.createStream('phishing') phishingStream.write({ hostname }) } + /** + * A method for providing our API over a stream using Dnode. + * @param {*} outStream - The stream to provide our API over. + */ setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) @@ -895,6 +1021,11 @@ module.exports = class MetamaskController extends EventEmitter { }) } + /** + * A method for serving our ethereum provider over a given stream. + * @param {*} outStream - The stream to provide over. + * @param {string} origin - The URI of the requesting resource. + */ setupProviderConnection (outStream, origin) { // setup json rpc engine stack const engine = new RpcEngine() @@ -924,6 +1055,16 @@ module.exports = class MetamaskController extends EventEmitter { ) } + /** + * A method for providing our public config info over a stream. + * This includes info we like to be synchronous if possible, like + * the current selected account, and network ID. + * + * Since synchronous methods have been deprecated in web3, + * this is a good candidate for deprecation. + * + * @param {*} outStream - The stream to provide public config over. + */ setupPublicConfig (outStream) { pump( asStream(this.publicConfigStore), @@ -934,10 +1075,21 @@ module.exports = class MetamaskController extends EventEmitter { ) } + /** + * A method for emitting the full MetaMask state to all registered listeners. + * @private + */ privateSendUpdate () { this.emit('update', this.getState()) } + /** + * A method for estimating a good gas price at recent prices. + * Returns the lowest price that would have been included in + * 50% of recent blocks. + * + * @returns {string} A hex representation of the suggested wei gas price. + */ getGasPrice () { const { recentBlocksController } = this const { recentBlocks } = recentBlocksController.store.getState() @@ -971,6 +1123,11 @@ module.exports = class MetamaskController extends EventEmitter { // Log blocks + /** + * A method for setting the user's preferred display currency. + * @param {string} currencyCode - The code of the preferred currency. + * @param {Function} cb - A callback function returning currency info. + */ setCurrentCurrency (currencyCode, cb) { try { this.currencyController.setCurrentCurrency(currencyCode) @@ -986,6 +1143,13 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * A method for forwarding the user to the easiest way to obtain ether, + * or the network "gas" currency, for the current selected network. + * + * @param {string} address - The address to fund. + * @param {string} amount - The amount of ether desired, as a base 10 string. + */ buyEth (address, amount) { if (!amount) amount = '5' const network = this.networkController.getNetworkState() @@ -993,18 +1157,33 @@ module.exports = class MetamaskController extends EventEmitter { if (url) this.platform.openWindow({ url }) } + /** + * A method for triggering a shapeshift currency transfer. + * @param {string} depositAddress - The address to deposit to. + * @property {string} depositType - An abbreviation of the type of crypto currency to be deposited. + */ createShapeShiftTx (depositAddress, depositType) { this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) } // network - async setCustomRpc (rpcTarget, rpcList) { + /** + * A method for selecting a custom URL for an ethereum RPC provider. + * @param {string} rpcTarget - A URL for a valid Ethereum RPC API. + * @returns {Promise<String>} - The RPC Target URL confirmed. + */ + async setCustomRpc (rpcTarget) { this.networkController.setRpcTarget(rpcTarget) await this.preferencesController.updateFrequentRpcList(rpcTarget) return rpcTarget } + /** + * Sets whether or not to use the blockie identicon format. + * @param {boolean} val - True for bockie, false for jazzicon. + * @param {Function} cb - A callback function called when complete. + */ setUseBlockie (val, cb) { try { this.preferencesController.setUseBlockie(val) @@ -1014,6 +1193,11 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * A method for setting a user's current locale, affecting the language rendered. + * @param {string} key - Locale identifier. + * @param {Function} cb - A callback function called when complete. + */ setCurrentLocale (key, cb) { try { this.preferencesController.setCurrentLocale(key) @@ -1023,6 +1207,11 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * A method for initializing storage the first time. + * @param {Object} initState - The default state to initialize with. + * @private + */ recordFirstTimeInfo (initState) { if (!('firstTimeInfo' in initState)) { initState.firstTimeInfo = { @@ -1032,11 +1221,21 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * A method for recording whether the MetaMask user interface is open or not. + * @private + * @param {boolean} open + */ set isClientOpen (open) { this._isClientOpen = open this.isClientOpenAndUnlocked = this.getState().isUnlocked && open } + /** + * A method for activating the retrieval of price data, which should only be fetched when the UI is visible. + * @private + * @param {boolean} active - True if price data should be getting fetched. + */ set isClientOpenAndUnlocked (active) { this.tokenRatesController.isActive = active } diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js index bea1fe3da..ffbf24a4b 100644 --- a/app/scripts/migrations/018.js +++ b/app/scripts/migrations/018.js @@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style */ const clone = require('clone') -const txStateHistoryHelper = require('../lib/tx-state-history-helper') +const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper') module.exports = { |