1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Merge pull request #4040 from MetaMask/dm-docs-2

Even more documentation for various controllers and libs.
This commit is contained in:
Dan Finlay 2018-04-24 09:51:18 -07:00 committed by GitHub
commit ac334d7b1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 764 additions and 54 deletions

View File

@ -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) {

View File

@ -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)
}

View File

@ -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

View File

@ -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) => {

View File

@ -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({

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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) => {

View File

@ -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