aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/scripts/background.js129
-rw-r--r--app/scripts/config.js44
-rw-r--r--app/scripts/contentscript.js42
-rw-r--r--app/scripts/controllers/balance.js56
-rw-r--r--app/scripts/controllers/blacklist.js53
-rw-r--r--app/scripts/controllers/computed-balances.js45
-rw-r--r--app/scripts/controllers/network/enums.js56
-rw-r--r--app/scripts/controllers/network/index.js2
-rw-r--r--app/scripts/controllers/network/network.js (renamed from app/scripts/controllers/network.js)27
-rw-r--r--app/scripts/controllers/network/util.js65
-rw-r--r--app/scripts/controllers/preferences.js4
-rw-r--r--app/scripts/controllers/recent-blocks.js68
-rw-r--r--app/scripts/controllers/token-rates.js8
-rw-r--r--app/scripts/edge-encryptor.js142
-rw-r--r--app/scripts/first-time-state.js19
-rw-r--r--app/scripts/inpage.js13
-rw-r--r--app/scripts/lib/account-tracker.js81
-rw-r--r--app/scripts/lib/config-manager.js23
-rw-r--r--app/scripts/lib/createProviderMiddleware.js6
-rw-r--r--app/scripts/lib/extractEthjsErrorMessage.js23
-rw-r--r--app/scripts/lib/getObjStructure.js17
-rw-r--r--app/scripts/lib/message-manager.js137
-rw-r--r--app/scripts/lib/notification-manager.js44
-rw-r--r--app/scripts/lib/pending-balance-calculator.js38
-rw-r--r--app/scripts/lib/personal-message-manager.js142
-rw-r--r--app/scripts/lib/port-stream.js37
-rw-r--r--app/scripts/lib/seed-phrase-verifier.js18
-rw-r--r--app/scripts/lib/setupMetamaskMeshMetrics.js3
-rw-r--r--app/scripts/lib/typed-message-manager.js137
-rw-r--r--app/scripts/metamask-controller.js461
-rw-r--r--app/scripts/popup-core.js25
31 files changed, 1631 insertions, 334 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 6550e8944..38b871bb5 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
@@ -172,6 +275,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 +316,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 +381,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 +406,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 a8470ed82..000000000
--- a/app/scripts/config.js
+++ /dev/null
@@ -1,44 +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
-
-module.exports = {
- network: {
- localhost: LOCALHOST_RPC_URL,
- mainnet: MAINET_RPC_URL,
- ropsten: ROPSTEN_RPC_URL,
- kovan: KOVAN_RPC_URL,
- rinkeby: RINKEBY_RPC_URL,
- },
- // Used for beta UI
- 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,
- },
-}
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index fe1766273..dbf1c6d4c 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -23,6 +23,9 @@ if (shouldInjectWeb3()) {
setupStreams()
}
+/**
+ * Creates a script tag that injects inpage.js
+ */
function setupInjection () {
try {
// inject in-page script
@@ -37,6 +40,10 @@ function setupInjection () {
}
}
+/**
+ * Sets up two-way communication streams between the
+ * browser extension and local per-page browser context
+ */
function setupStreams () {
// setup communication to page and plugin
const pageStream = new LocalMessageDuplexStream({
@@ -89,17 +96,34 @@ function setupStreams () {
mux.ignoreStream('publicConfig')
}
+
+/**
+ * Error handler for page to plugin stream disconnections
+ *
+ * @param {string} remoteLabel Remote stream name
+ * @param {Error} err Stream connection error
+ */
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
}
+/**
+ * Determines if Web3 should be injected
+ *
+ * @returns {boolean} {@code true} if Web3 should be injected
+ */
function shouldInjectWeb3 () {
return doctypeCheck() && suffixCheck()
&& documentElementCheck() && !blacklistedDomainCheck()
}
+/**
+ * Checks the doctype of the current document if it exists
+ *
+ * @returns {boolean} {@code true} if the doctype is html or if none exists
+ */
function doctypeCheck () {
const doctype = window.document.doctype
if (doctype) {
@@ -109,6 +133,11 @@ function doctypeCheck () {
}
}
+/**
+ * Checks the current document extension
+ *
+ * @returns {boolean} {@code true} if the current extension is not prohibited
+ */
function suffixCheck () {
var prohibitedTypes = ['xml', 'pdf']
var currentUrl = window.location.href
@@ -122,6 +151,11 @@ function suffixCheck () {
return true
}
+/**
+ * Checks the documentElement of the current document
+ *
+ * @returns {boolean} {@code true} if the documentElement is an html node or if none exists
+ */
function documentElementCheck () {
var documentElement = document.documentElement.nodeName
if (documentElement) {
@@ -130,6 +164,11 @@ function documentElementCheck () {
return true
}
+/**
+ * Checks if the current domain is blacklisted
+ *
+ * @returns {boolean} {@code true} if the current domain is blacklisted
+ */
function blacklistedDomainCheck () {
var blacklistedDomains = [
'uscourts.gov',
@@ -148,6 +187,9 @@ function blacklistedDomainCheck () {
return false
}
+/**
+ * Redirects the current page to a phishing information page
+ */
function redirectToPhishingWarning () {
console.log('MetaMask - redirecting to phishing warning')
window.location.href = 'https://metamask.io/phishing.html'
diff --git a/app/scripts/controllers/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/computed-balances.js b/app/scripts/controllers/computed-balances.js
index 907b087cf..1a6802f9a 100644
--- a/app/scripts/controllers/computed-balances.js
+++ b/app/scripts/controllers/computed-balances.js
@@ -2,8 +2,24 @@ const ObservableStore = require('obs-store')
const extend = require('xtend')
const BalanceController = require('./balance')
-class ComputedbalancesController {
+/**
+ * @typedef {Object} ComputedBalancesOptions
+ * @property {Object} accountTracker Account tracker store reference
+ * @property {Object} txController Token controller reference
+ * @property {Object} blockTracker Block tracker reference
+ * @property {Object} initState Initial state to populate this internal store with
+ */
+/**
+ * Background controller responsible for syncing
+ * and computing ETH balances for all accounts
+ */
+class ComputedbalancesController {
+ /**
+ * Creates a new controller instance
+ *
+ * @param {ComputedBalancesOptions} [opts] Controller configuration parameters
+ */
constructor (opts = {}) {
const { accountTracker, txController, blockTracker } = opts
this.accountTracker = accountTracker
@@ -19,6 +35,9 @@ class ComputedbalancesController {
this._initBalanceUpdating()
}
+ /**
+ * Updates balances associated with each internal address
+ */
updateAllBalances () {
Object.keys(this.balances).forEach((balance) => {
const address = balance.address
@@ -26,12 +45,23 @@ class ComputedbalancesController {
})
}
+ /**
+ * Initializes internal address tracking
+ *
+ * @private
+ */
_initBalanceUpdating () {
const store = this.accountTracker.store.getState()
this.syncAllAccountsFromStore(store)
this.accountTracker.store.subscribe(this.syncAllAccountsFromStore.bind(this))
}
+ /**
+ * Uses current account state to sync and track all
+ * addresses associated with the current account
+ *
+ * @param {{ accounts: Object }} store Account tracking state
+ */
syncAllAccountsFromStore (store) {
const upstream = Object.keys(store.accounts)
const balances = Object.keys(this.balances)
@@ -50,6 +80,13 @@ class ComputedbalancesController {
})
}
+ /**
+ * Conditionally establishes a new subscription
+ * to track an address associated with the current
+ * account
+ *
+ * @param {string} address Address to conditionally subscribe to
+ */
trackAddressIfNotAlready (address) {
const state = this.store.getState()
if (!(address in state.computedBalances)) {
@@ -57,6 +94,12 @@ class ComputedbalancesController {
}
}
+ /**
+ * Establishes a new subscription to track an
+ * address associated with the current account
+ *
+ * @param {string} address Address to conditionally subscribe to
+ */
trackAddress (address) {
const updater = new BalanceController({
address,
diff --git a/app/scripts/controllers/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..6fd983bb2 100644
--- a/app/scripts/controllers/network.js
+++ b/app/scripts/controllers/network/network.js
@@ -7,11 +7,18 @@ 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 22e3e8154..21384f262 100644
--- a/app/scripts/controllers/token-rates.js
+++ b/app/scripts/controllers/token-rates.js
@@ -39,14 +39,14 @@ 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.dev.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
const json = await response.json()
return json && json.length ? json[0].averagePrice : 0
} catch (error) { }
}
/**
- * @type {Number} - Interval used to poll for exchange rates
+ * @type {Number}
*/
set interval (interval) {
this._handle && clearInterval(this._handle)
@@ -55,7 +55,7 @@ class TokenRatesController {
}
/**
- * @type {Object} - Preferences controller instance
+ * @type {Object}
*/
set preferences (preferences) {
this._preferences && this._preferences.unsubscribe()
@@ -66,7 +66,7 @@ class TokenRatesController {
}
/**
- * @type {Array} - Array of token objects with contract addresses
+ * @type {Array}
*/
set tokens (tokens) {
this._tokens = tokens
diff --git a/app/scripts/edge-encryptor.js b/app/scripts/edge-encryptor.js
index 24c0c93a8..dcb06873b 100644
--- a/app/scripts/edge-encryptor.js
+++ b/app/scripts/edge-encryptor.js
@@ -1,69 +1,97 @@
const asmcrypto = require('asmcrypto.js')
const Unibabel = require('browserify-unibabel')
+/**
+ * A Microsoft Edge-specific encryption class that exposes
+ * the interface expected by eth-keykeyring-controller
+ */
class EdgeEncryptor {
+ /**
+ * Encrypts an arbitrary object to ciphertext
+ *
+ * @param {string} password Used to generate a key to encrypt the data
+ * @param {Object} dataObject Data to encrypt
+ * @returns {Promise<string>} Promise resolving to an object with ciphertext
+ */
+ encrypt (password, dataObject) {
+ var salt = this._generateSalt()
+ return this._keyFromPassword(password, salt)
+ .then(function (key) {
+ var data = JSON.stringify(dataObject)
+ var dataBuffer = Unibabel.utf8ToBuffer(data)
+ var vector = global.crypto.getRandomValues(new Uint8Array(16))
+ var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector)
- encrypt (password, dataObject) {
+ var buffer = new Uint8Array(resultbuffer)
+ var vectorStr = Unibabel.bufferToBase64(vector)
+ var vaultStr = Unibabel.bufferToBase64(buffer)
+ return JSON.stringify({
+ data: vaultStr,
+ iv: vectorStr,
+ salt: salt,
+ })
+ })
+ }
- var salt = this._generateSalt()
- return this._keyFromPassword(password, salt)
- .then(function (key) {
+ /**
+ * Decrypts an arbitrary object from ciphertext
+ *
+ * @param {string} password Used to generate a key to decrypt the data
+ * @param {string} text Ciphertext of an encrypted object
+ * @returns {Promise<Object>} Promise resolving to copy of decrypted object
+ */
+ decrypt (password, text) {
+ const payload = JSON.parse(text)
+ const salt = payload.salt
+ return this._keyFromPassword(password, salt)
+ .then(function (key) {
+ const encryptedData = Unibabel.base64ToBuffer(payload.data)
+ const vector = Unibabel.base64ToBuffer(payload.iv)
+ return new Promise((resolve, reject) => {
+ var result
+ try {
+ result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector)
+ } catch (err) {
+ return reject(new Error('Incorrect password'))
+ }
+ const decryptedData = new Uint8Array(result)
+ const decryptedStr = Unibabel.bufferToUtf8(decryptedData)
+ const decryptedObj = JSON.parse(decryptedStr)
+ resolve(decryptedObj)
+ })
+ })
+ }
- var data = JSON.stringify(dataObject)
- var dataBuffer = Unibabel.utf8ToBuffer(data)
- var vector = global.crypto.getRandomValues(new Uint8Array(16))
- var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector)
+ /**
+ * Retrieves a cryptographic key using a password
+ *
+ * @private
+ * @param {string} password Password used to unlock a cryptographic key
+ * @param {string} salt Random base64 data
+ * @returns {Promise<Object>} Promise resolving to a derived key
+ */
+ _keyFromPassword (password, salt) {
- var buffer = new Uint8Array(resultbuffer)
- var vectorStr = Unibabel.bufferToBase64(vector)
- var vaultStr = Unibabel.bufferToBase64(buffer)
- return JSON.stringify({
- data: vaultStr,
- iv: vectorStr,
- salt: salt,
- })
- })
- }
+ var passBuffer = Unibabel.utf8ToBuffer(password)
+ var saltBuffer = Unibabel.base64ToBuffer(salt)
+ return new Promise((resolve) => {
+ var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000)
+ resolve(key)
+ })
+ }
- decrypt (password, text) {
-
- const payload = JSON.parse(text)
- const salt = payload.salt
- return this._keyFromPassword(password, salt)
- .then(function (key) {
- const encryptedData = Unibabel.base64ToBuffer(payload.data)
- const vector = Unibabel.base64ToBuffer(payload.iv)
- return new Promise((resolve, reject) => {
- var result
- try {
- result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector)
- } catch (err) {
- return reject(new Error('Incorrect password'))
- }
- const decryptedData = new Uint8Array(result)
- const decryptedStr = Unibabel.bufferToUtf8(decryptedData)
- const decryptedObj = JSON.parse(decryptedStr)
- resolve(decryptedObj)
- })
- })
- }
-
- _keyFromPassword (password, salt) {
-
- var passBuffer = Unibabel.utf8ToBuffer(password)
- var saltBuffer = Unibabel.base64ToBuffer(salt)
- return new Promise((resolve) => {
- var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000)
- resolve(key)
- })
- }
-
- _generateSalt (byteCount = 32) {
- var view = new Uint8Array(byteCount)
- global.crypto.getRandomValues(view)
- var b64encoded = btoa(String.fromCharCode.apply(null, view))
- return b64encoded
- }
+ /**
+ * Generates random base64 encoded data
+ *
+ * @private
+ * @returns {string} Randomized base64 encoded data
+ */
+ _generateSalt (byteCount = 32) {
+ var view = new Uint8Array(byteCount)
+ global.crypto.getRandomValues(view)
+ var b64encoded = btoa(String.fromCharCode.apply(null, view))
+ return b64encoded
+ }
}
module.exports = EdgeEncryptor
diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js
index 3063df627..c49d89288 100644
--- a/app/scripts/first-time-state.js
+++ b/app/scripts/first-time-state.js
@@ -1,15 +1,24 @@
// 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')
-//
-// The default state of MetaMask
-//
-module.exports = {
+/**
+ * @typedef {Object} FirstTimeState
+ * @property {Object} config Initial configuration parameters
+ * @property {Object} NetworkController Network controller state
+ */
+
+/**
+ * @type {FirstTimeState}
+ */
+const initialState = {
config: {},
NetworkController: {
provider: {
- type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet',
+ type: (METAMASK_DEBUG || env === 'test') ? DEFAULT_NETWORK : MAINNET,
},
},
}
+
+module.exports = initialState
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index 92c732813..6d16eebd4 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -42,20 +42,20 @@ log.debug('MetaMask - injected web3')
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// set web3 defaultAccount
-
inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress
})
-//
-// util
-//
-
// need to make sure we aren't affected by overlapping namespaces
// and that we dont affect the app with our namespace
// mostly a fix for web3's BigNumber if AMD's "define" is defined...
var __define
+/**
+ * Caches reference to global define object and deletes it to
+ * avoid conflicts with other global define objects, such as
+ * AMD's define function
+ */
function cleanContextForImports () {
__define = global.define
try {
@@ -65,6 +65,9 @@ function cleanContextForImports () {
}
}
+/**
+ * Restores global define object from cached reference
+ */
function restoreContextAfterImports () {
try {
global.define = __define
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 63d27c40e..c10ff2f4e 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.
@@ -174,19 +173,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/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js
index 4e667bac2..8a939ba4e 100644
--- a/app/scripts/lib/createProviderMiddleware.js
+++ b/app/scripts/lib/createProviderMiddleware.js
@@ -1,6 +1,10 @@
module.exports = createProviderMiddleware
-// forward requests to provider
+/**
+ * Forwards an HTTP request to the current Web3 provider
+ *
+ * @param {{ provider: Object }} config Configuration containing current Web3 provider
+ */
function createProviderMiddleware ({ provider }) {
return (req, res, next, end) => {
provider.sendAsync(req, (err, _res) => {
diff --git a/app/scripts/lib/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/port-stream.js b/app/scripts/lib/port-stream.js
index a9716fb00..5c4224fd9 100644
--- a/app/scripts/lib/port-stream.js
+++ b/app/scripts/lib/port-stream.js
@@ -6,6 +6,13 @@ module.exports = PortDuplexStream
inherits(PortDuplexStream, Duplex)
+/**
+ * Creates a stream that's both readable and writable.
+ * The stream supports arbitrary objects.
+ *
+ * @class
+ * @param {Object} port Remote Port object
+ */
function PortDuplexStream (port) {
Duplex.call(this, {
objectMode: true,
@@ -15,8 +22,13 @@ function PortDuplexStream (port) {
port.onDisconnect.addListener(this._onDisconnect.bind(this))
}
-// private
-
+/**
+ * Callback triggered when a message is received from
+ * the remote Port associated with this Stream.
+ *
+ * @private
+ * @param {Object} msg - Payload from the onMessage listener of Port
+ */
PortDuplexStream.prototype._onMessage = function (msg) {
if (Buffer.isBuffer(msg)) {
delete msg._isBuffer
@@ -27,14 +39,31 @@ PortDuplexStream.prototype._onMessage = function (msg) {
}
}
+/**
+ * Callback triggered when the remote Port
+ * associated with this Stream disconnects.
+ *
+ * @private
+ */
PortDuplexStream.prototype._onDisconnect = function () {
this.destroy()
}
-// stream plumbing
-
+/**
+ * Explicitly sets read operations to a no-op
+ */
PortDuplexStream.prototype._read = noop
+
+/**
+ * Called internally when data should be written to
+ * this writable stream.
+ *
+ * @private
+ * @param {*} msg Arbitrary object to write
+ * @param {string} encoding Encoding to use when writing payload
+ * @param {Function} cb Called when writing is complete or an error occurs
+ */
PortDuplexStream.prototype._write = function (msg, encoding, cb) {
try {
if (Buffer.isBuffer(msg)) {
diff --git a/app/scripts/lib/seed-phrase-verifier.js b/app/scripts/lib/seed-phrase-verifier.js
index 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/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js
index 40343f017..02690a948 100644
--- a/app/scripts/lib/setupMetamaskMeshMetrics.js
+++ b/app/scripts/lib/setupMetamaskMeshMetrics.js
@@ -1,6 +1,9 @@
module.exports = setupMetamaskMeshMetrics
+/**
+ * Injects an iframe into the current document for testing
+ */
function setupMetamaskMeshMetrics() {
const testingContainer = document.createElement('iframe')
testingContainer.src = 'https://metamask.github.io/mesh-testing/'
diff --git a/app/scripts/lib/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 782bc50ac..edde38819 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
@@ -314,9 +315,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
@@ -407,16 +410,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()
@@ -442,7 +447,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
*/
@@ -460,10 +465,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) {
@@ -472,12 +483,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
*/
@@ -507,6 +518,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) {
@@ -526,6 +539,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 () {
@@ -556,6 +571,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)
@@ -563,9 +579,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)
@@ -577,11 +597,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)
@@ -595,13 +617,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
@@ -620,14 +671,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()
@@ -636,7 +710,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)}`))
}
@@ -644,7 +718,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')
@@ -665,7 +743,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')
@@ -685,12 +810,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) {
@@ -700,8 +843,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) {
@@ -714,12 +875,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)
@@ -733,113 +905,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()
@@ -850,6 +946,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)) {
@@ -865,6 +968,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)
@@ -873,12 +986,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)
@@ -897,6 +1023,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()
@@ -926,6 +1057,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),
@@ -936,10 +1077,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()
@@ -973,6 +1125,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)
@@ -988,6 +1145,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()
@@ -995,18 +1159,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)
@@ -1016,6 +1195,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)
@@ -1025,6 +1209,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 = {
@@ -1034,11 +1223,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/popup-core.js b/app/scripts/popup-core.js
index 2e4334bb1..6325b8a8d 100644
--- a/app/scripts/popup-core.js
+++ b/app/scripts/popup-core.js
@@ -7,10 +7,14 @@ const launchMetamaskUi = require('../../ui')
const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
-
module.exports = initializePopup
-
+/**
+ * Asynchronously initializes the MetaMask popup UI
+ *
+ * @param {{ container: Element, connectionStream: * }} config Popup configuration object
+ * @param {Function} cb Called when initialization is complete
+ */
function initializePopup ({ container, connectionStream }, cb) {
// setup app
async.waterfall([
@@ -19,6 +23,12 @@ function initializePopup ({ container, connectionStream }, cb) {
], cb)
}
+/**
+ * Establishes streamed connections to background scripts and a Web3 provider
+ *
+ * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
+ * @param {Function} cb Called when controller connection is established
+ */
function connectToAccountManager (connectionStream, cb) {
// setup communication with background
// setup multiplexing
@@ -28,6 +38,11 @@ function connectToAccountManager (connectionStream, cb) {
setupWeb3Connection(mx.createStream('provider'))
}
+/**
+ * Establishes a streamed connection to a Web3 provider
+ *
+ * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
+ */
function setupWeb3Connection (connectionStream) {
var providerStream = new StreamProvider()
providerStream.pipe(connectionStream).pipe(providerStream)
@@ -38,6 +53,12 @@ function setupWeb3Connection (connectionStream) {
global.eth = new Eth(providerStream)
}
+/**
+ * Establishes a streamed connection to the background account manager
+ *
+ * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
+ * @param {Function} cb Called when the remote account manager connection is established
+ */
function setupControllerConnection (connectionStream, cb) {
// this is a really sneaky way of adding EventEmitter api
// to a bi-directional dnode instance