aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/scripts/lib')
-rw-r--r--app/scripts/lib/ComposableObservableStore.js49
-rw-r--r--app/scripts/lib/account-tracker.js81
-rw-r--r--app/scripts/lib/buy-eth-url.js11
-rw-r--r--app/scripts/lib/config-manager.js23
-rw-r--r--app/scripts/lib/createLoggerMiddleware.js16
-rw-r--r--app/scripts/lib/createOriginMiddleware.js12
-rw-r--r--app/scripts/lib/createProviderMiddleware.js6
-rw-r--r--app/scripts/lib/enums.js9
-rw-r--r--app/scripts/lib/environment-type.js10
-rw-r--r--app/scripts/lib/events-proxy.js25
-rw-r--r--app/scripts/lib/extractEthjsErrorMessage.js23
-rw-r--r--app/scripts/lib/get-first-preferred-lang-code.js13
-rw-r--r--app/scripts/lib/getObjStructure.js50
-rw-r--r--app/scripts/lib/hex-to-bn.js7
-rw-r--r--app/scripts/lib/is-popup-or-notification.js11
-rw-r--r--app/scripts/lib/local-store.js38
-rw-r--r--app/scripts/lib/message-manager.js137
-rw-r--r--app/scripts/lib/migrator/index.js62
-rw-r--r--app/scripts/lib/nodeify.js8
-rw-r--r--app/scripts/lib/nonce-tracker.js148
-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/pending-tx-tracker.js189
-rw-r--r--app/scripts/lib/personal-message-manager.js143
-rw-r--r--app/scripts/lib/port-stream.js37
-rw-r--r--app/scripts/lib/seed-phrase-verifier.js19
-rw-r--r--app/scripts/lib/setupMetamaskMeshMetrics.js3
-rw-r--r--app/scripts/lib/setupRaven.js61
-rw-r--r--app/scripts/lib/stream-utils.js18
-rw-r--r--app/scripts/lib/tx-gas-utils.js136
-rw-r--r--app/scripts/lib/tx-state-history-helper.js41
-rw-r--r--app/scripts/lib/tx-state-manager.js297
-rw-r--r--app/scripts/lib/typed-message-manager.js137
-rw-r--r--app/scripts/lib/util.js81
34 files changed, 1028 insertions, 955 deletions
diff --git a/app/scripts/lib/ComposableObservableStore.js b/app/scripts/lib/ComposableObservableStore.js
new file mode 100644
index 000000000..d5ee708a1
--- /dev/null
+++ b/app/scripts/lib/ComposableObservableStore.js
@@ -0,0 +1,49 @@
+const ObservableStore = require('obs-store')
+
+/**
+ * An ObservableStore that can composes a flat
+ * structure of child stores based on configuration
+ */
+class ComposableObservableStore extends ObservableStore {
+ /**
+ * Create a new store
+ *
+ * @param {Object} [initState] - The initial store state
+ * @param {Object} [config] - Map of internal state keys to child stores
+ */
+ constructor (initState, config) {
+ super(initState)
+ this.updateStructure(config)
+ }
+
+ /**
+ * Composes a new internal store subscription structure
+ *
+ * @param {Object} [config] - Map of internal state keys to child stores
+ */
+ updateStructure (config) {
+ this.config = config
+ this.removeAllListeners()
+ for (const key in config) {
+ config[key].subscribe((state) => {
+ this.updateState({ [key]: state })
+ })
+ }
+ }
+
+ /**
+ * Merges all child store state into a single object rather than
+ * returning an object keyed by child store class name
+ *
+ * @returns {Object} - Object containing merged child store state
+ */
+ getFlatState () {
+ let flatState = {}
+ for (const key in this.config) {
+ flatState = { ...flatState, ...this.config[key].getState() }
+ }
+ return flatState
+ }
+}
+
+module.exports = ComposableObservableStore
diff --git a/app/scripts/lib/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/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js
index b9dde3c28..4e2d0bc79 100644
--- a/app/scripts/lib/buy-eth-url.js
+++ b/app/scripts/lib/buy-eth-url.js
@@ -1,5 +1,16 @@
module.exports = getBuyEthUrl
+/**
+ * Gives the caller a url at which the user can acquire eth, depending on the network they are in
+ *
+ * @param {object} opts Options required to determine the correct url
+ * @param {string} opts.network The network for which to return a url
+ * @param {string} opts.amount The amount of ETH to buy on coinbase. Only relevant if network === '1'.
+ * @param {string} opts.address The address the bought ETH should be sent to. Only relevant if network === '1'.
+ * @returns {string|undefined} The url at which the user can access ETH, while in the given network. If the passed
+ * network does not match any of the specified cases, or if no network is given, returns undefined.
+ *
+ */
function getBuyEthUrl ({ network, amount, address }) {
let url
switch (network) {
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index 34b603b96..221746467 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -1,12 +1,11 @@
const ethUtil = require('ethereumjs-util')
const normalize = require('eth-sig-util').normalize
-const MetamaskConfig = require('../config.js')
-
-
-const MAINNET_RPC = MetamaskConfig.network.mainnet
-const ROPSTEN_RPC = MetamaskConfig.network.ropsten
-const KOVAN_RPC = MetamaskConfig.network.kovan
-const RINKEBY_RPC = MetamaskConfig.network.rinkeby
+const {
+ MAINNET_RPC_URL,
+ ROPSTEN_RPC_URL,
+ KOVAN_RPC_URL,
+ RINKEBY_RPC_URL,
+} = require('../controllers/network/enums')
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@@ -154,19 +153,19 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
switch (provider.type) {
case 'mainnet':
- return MAINNET_RPC
+ return MAINNET_RPC_URL
case 'ropsten':
- return ROPSTEN_RPC
+ return ROPSTEN_RPC_URL
case 'kovan':
- return KOVAN_RPC
+ return KOVAN_RPC_URL
case 'rinkeby':
- return RINKEBY_RPC
+ return RINKEBY_RPC_URL
default:
- return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC
+ return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL
}
}
diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js
index 2707cbd9e..996c3477c 100644
--- a/app/scripts/lib/createLoggerMiddleware.js
+++ b/app/scripts/lib/createLoggerMiddleware.js
@@ -1,14 +1,20 @@
-// log rpc activity
+const log = require('loglevel')
+
module.exports = createLoggerMiddleware
-function createLoggerMiddleware ({ origin }) {
- return function loggerMiddleware (req, res, next, end) {
- next((cb) => {
+/**
+ * Returns a middleware that logs RPC activity
+ * @param {{ origin: string }} opts - The middleware options
+ * @returns {Function}
+ */
+function createLoggerMiddleware (opts) {
+ return function loggerMiddleware (/** @type {any} */ req, /** @type {any} */ res, /** @type {Function} */ next) {
+ next((/** @type {Function} */ cb) => {
if (res.error) {
log.error('Error in RPC response:\n', res)
}
if (req.isMetamaskInternal) return
- log.info(`RPC (${origin}):`, req, '->', res)
+ log.info(`RPC (${opts.origin}):`, req, '->', res)
cb()
})
}
diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js
index f8bdb2dc2..98bb0e3b3 100644
--- a/app/scripts/lib/createOriginMiddleware.js
+++ b/app/scripts/lib/createOriginMiddleware.js
@@ -1,9 +1,13 @@
-// append dapp origin domain to request
module.exports = createOriginMiddleware
-function createOriginMiddleware ({ origin }) {
- return function originMiddleware (req, res, next, end) {
- req.origin = origin
+/**
+ * Returns a middleware that appends the DApp origin to request
+ * @param {{ origin: string }} opts - The middleware options
+ * @returns {Function}
+ */
+function createOriginMiddleware (opts) {
+ return function originMiddleware (/** @type {any} */ req, /** @type {any} */ _, /** @type {Function} */ next) {
+ req.origin = opts.origin
next()
}
}
diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js
index 4e667bac2..8a939ba4e 100644
--- a/app/scripts/lib/createProviderMiddleware.js
+++ b/app/scripts/lib/createProviderMiddleware.js
@@ -1,6 +1,10 @@
module.exports = createProviderMiddleware
-// forward requests to provider
+/**
+ * Forwards an HTTP request to the current Web3 provider
+ *
+ * @param {{ provider: Object }} config Configuration containing current Web3 provider
+ */
function createProviderMiddleware ({ provider }) {
return (req, res, next, end) => {
provider.sendAsync(req, (err, _res) => {
diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js
new file mode 100644
index 000000000..0a3afca47
--- /dev/null
+++ b/app/scripts/lib/enums.js
@@ -0,0 +1,9 @@
+const ENVIRONMENT_TYPE_POPUP = 'popup'
+const ENVIRONMENT_TYPE_NOTIFICATION = 'notification'
+const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen'
+
+module.exports = {
+ ENVIRONMENT_TYPE_POPUP,
+ ENVIRONMENT_TYPE_NOTIFICATION,
+ ENVIRONMENT_TYPE_FULLSCREEN,
+}
diff --git a/app/scripts/lib/environment-type.js b/app/scripts/lib/environment-type.js
deleted file mode 100644
index 7966926eb..000000000
--- a/app/scripts/lib/environment-type.js
+++ /dev/null
@@ -1,10 +0,0 @@
-module.exports = function environmentType () {
- const url = window.location.href
- if (url.match(/popup.html$/)) {
- return 'popup'
- } else if (url.match(/home.html$/)) {
- return 'responsive'
- } else {
- return 'notification'
- }
-}
diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js
index c0a490b05..f83773ccc 100644
--- a/app/scripts/lib/events-proxy.js
+++ b/app/scripts/lib/events-proxy.js
@@ -1,26 +1,37 @@
+/**
+ * Returns an EventEmitter that proxies events from the given event emitter
+ * @param {any} eventEmitter
+ * @param {object} listeners - The listeners to proxy to
+ * @returns {any}
+ */
module.exports = function createEventEmitterProxy (eventEmitter, listeners) {
let target = eventEmitter
const eventHandlers = listeners || {}
- const proxy = new Proxy({}, {
- get: (obj, name) => {
+ const proxy = /** @type {any} */ (new Proxy({}, {
+ get: (_, name) => {
// intercept listeners
if (name === 'on') return addListener
if (name === 'setTarget') return setTarget
if (name === 'proxyEventHandlers') return eventHandlers
- return target[name]
+ return (/** @type {any} */ (target))[name]
},
- set: (obj, name, value) => {
+ set: (_, name, value) => {
target[name] = value
return true
},
- })
- function setTarget (eventEmitter) {
+ }))
+ function setTarget (/** @type {EventEmitter} */ eventEmitter) {
target = eventEmitter
// migrate listeners
Object.keys(eventHandlers).forEach((name) => {
- eventHandlers[name].forEach((handler) => target.on(name, handler))
+ /** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler))
})
}
+ /**
+ * Attaches a function to be called whenever the specified event is emitted
+ * @param {string} name
+ * @param {Function} handler
+ */
function addListener (name, handler) {
if (!eventHandlers[name]) eventHandlers[name] = []
eventHandlers[name].push(handler)
diff --git a/app/scripts/lib/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/get-first-preferred-lang-code.js b/app/scripts/lib/get-first-preferred-lang-code.js
index 28612f763..5473fccf0 100644
--- a/app/scripts/lib/get-first-preferred-lang-code.js
+++ b/app/scripts/lib/get-first-preferred-lang-code.js
@@ -2,14 +2,23 @@ const extension = require('extensionizer')
const promisify = require('pify')
const allLocales = require('../../_locales/index.json')
-const existingLocaleCodes = allLocales.map(locale => locale.code)
+const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-'))
+/**
+ * Returns a preferred language code, based on settings within the user's browser. If we have no translations for the
+ * users preferred locales, 'en' is returned.
+ *
+ * @returns {Promise<string>} Promises a locale code, either one from the user's preferred list that we have a translation for, or 'en'
+ *
+ */
async function getFirstPreferredLangCode () {
const userPreferredLocaleCodes = await promisify(
extension.i18n.getAcceptLanguages,
{ errorFirst: false }
)()
- const firstPreferredLangCode = userPreferredLocaleCodes.find(code => existingLocaleCodes.includes(code))
+ const firstPreferredLangCode = userPreferredLocaleCodes
+ .map(code => code.toLowerCase())
+ .find(code => existingLocaleCodes.includes(code))
return firstPreferredLangCode || 'en'
}
diff --git a/app/scripts/lib/getObjStructure.js b/app/scripts/lib/getObjStructure.js
new file mode 100644
index 000000000..52250d3fb
--- /dev/null
+++ b/app/scripts/lib/getObjStructure.js
@@ -0,0 +1,50 @@
+const clone = require('clone')
+
+module.exports = getObjStructure
+
+// This will create an object that represents the structure of the given object
+// it replaces all values with the result of their type
+
+// {
+// "data": {
+// "CurrencyController": {
+// "conversionDate": "number",
+// "conversionRate": "number",
+// "currentCurrency": "string"
+// }
+// }
+
+/**
+ * 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) => {
+ return value === null ? 'null' : typeof value
+ })
+}
+
+/**
+ * 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) {
+ target[key] = deepMap(value, visit)
+ } else {
+ target[key] = visit(value)
+ }
+ })
+ return target
+}
diff --git a/app/scripts/lib/hex-to-bn.js b/app/scripts/lib/hex-to-bn.js
index 184217279..b28746920 100644
--- a/app/scripts/lib/hex-to-bn.js
+++ b/app/scripts/lib/hex-to-bn.js
@@ -1,6 +1,11 @@
-const ethUtil = require('ethereumjs-util')
+const ethUtil = (/** @type {object} */ (require('ethereumjs-util')))
const BN = ethUtil.BN
+/**
+ * Returns a [BinaryNumber]{@link BN} representation of the given hex value
+ * @param {string} hex
+ * @return {any}
+ */
module.exports = function hexToBn (hex) {
return new BN(ethUtil.stripHexPrefix(hex), 16)
}
diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js
deleted file mode 100644
index e2999411f..000000000
--- a/app/scripts/lib/is-popup-or-notification.js
+++ /dev/null
@@ -1,11 +0,0 @@
-module.exports = function isPopupOrNotification () {
- const url = window.location.href
- // if (url.match(/popup.html$/) || url.match(/home.html$/)) {
- // Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js)
- // Revert below regexes to above commented out regexes before merge to master
- if (url.match(/popup.html(?:\?.+)*$/) || url.match(/home.html(?:\?.+)*$/)) {
- return 'popup'
- } else {
- return 'notification'
- }
-}
diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js
index 5b47985f6..139ff86bd 100644
--- a/app/scripts/lib/local-store.js
+++ b/app/scripts/lib/local-store.js
@@ -1,10 +1,13 @@
-// We should not rely on local storage in an extension!
-// We should use this instead!
-// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/local
-
const extension = require('extensionizer')
+const log = require('loglevel')
+/**
+ * A wrapper around the extension's storage local API
+ */
module.exports = class ExtensionStore {
+ /**
+ * @constructor
+ */
constructor() {
this.isSupported = !!(extension.storage.local)
if (!this.isSupported) {
@@ -12,6 +15,10 @@ module.exports = class ExtensionStore {
}
}
+ /**
+ * Returns all of the keys currently saved
+ * @return {Promise<*>}
+ */
async get() {
if (!this.isSupported) return undefined
const result = await this._get()
@@ -24,14 +31,24 @@ module.exports = class ExtensionStore {
}
}
+ /**
+ * Sets the key in local state
+ * @param {object} state - The state to set
+ * @return {Promise<void>}
+ */
async set(state) {
return this._set(state)
}
+ /**
+ * Returns all of the keys currently saved
+ * @private
+ * @return {object} the key-value map from local storage
+ */
_get() {
const local = extension.storage.local
return new Promise((resolve, reject) => {
- local.get(null, (result) => {
+ local.get(null, (/** @type {any} */ result) => {
const err = extension.runtime.lastError
if (err) {
reject(err)
@@ -42,6 +59,12 @@ module.exports = class ExtensionStore {
})
}
+ /**
+ * Sets the key in local state
+ * @param {object} obj - The key to set
+ * @return {Promise<void>}
+ * @private
+ */
_set(obj) {
const local = extension.storage.local
return new Promise((resolve, reject) => {
@@ -57,6 +80,11 @@ module.exports = class ExtensionStore {
}
}
+/**
+ * Returns whether or not the given object contains no keys
+ * @param {object} obj - The object to check
+ * @returns {boolean}
+ */
function isEmpty(obj) {
return Object.keys(obj).length === 0
}
diff --git a/app/scripts/lib/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/migrator/index.js b/app/scripts/lib/migrator/index.js
index 4fd2cae92..345ca8001 100644
--- a/app/scripts/lib/migrator/index.js
+++ b/app/scripts/lib/migrator/index.js
@@ -1,6 +1,25 @@
-class Migrator {
+const EventEmitter = require('events')
+/**
+ * @typedef {object} Migration
+ * @property {number} version - The migration version
+ * @property {Function} migrate - Returns a promise of the migrated data
+ */
+
+/**
+ * @typedef {object} MigratorOptions
+ * @property {Array<Migration>} [migrations] - The list of migrations to apply
+ * @property {number} [defaultVersion] - The version to use in the initial state
+ */
+
+class Migrator extends EventEmitter {
+
+ /**
+ * @constructor
+ * @param {MigratorOptions} opts
+ */
constructor (opts = {}) {
+ super()
const migrations = opts.migrations || []
// sort migrations by version
this.migrations = migrations.sort((a, b) => a.version - b.version)
@@ -12,30 +31,57 @@ class Migrator {
// run all pending migrations on meta in place
async migrateData (versionedData = this.generateInitialState()) {
+ // get all migrations that have not yet been run
const pendingMigrations = this.migrations.filter(migrationIsPending)
+ // perform each migration
for (const index in pendingMigrations) {
const migration = pendingMigrations[index]
- versionedData = await migration.migrate(versionedData)
- if (!versionedData.data) throw new Error('Migrator - migration returned empty data')
- if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
+ try {
+ // attempt migration and validate
+ const migratedData = await migration.migrate(versionedData)
+ if (!migratedData.data) throw new Error('Migrator - migration returned empty data')
+ if (migratedData.version !== undefined && migratedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
+ // accept the migration as good
+ versionedData = migratedData
+ } catch (err) {
+ // rewrite error message to add context without clobbering stack
+ const originalErrorMessage = err.message
+ err.message = `MetaMask Migration Error #${migration.version}: ${originalErrorMessage}`
+ console.warn(err.stack)
+ // emit error instead of throw so as to not break the run (gracefully fail)
+ this.emit('error', err)
+ // stop migrating and use state as is
+ return versionedData
+ }
}
return versionedData
- // migration is "pending" if it has a higher
- // version number than currentVersion
+ /**
+ * Returns whether or not the migration is pending
+ *
+ * A migration is considered "pending" if it has a higher
+ * version number than the current version.
+ * @param {Migration} migration
+ * @returns {boolean}
+ */
function migrationIsPending (migration) {
return migration.version > versionedData.meta.version
}
}
- generateInitialState (initState) {
+ /**
+ * Returns the initial state for the migrator
+ * @param {object} [data] - The data for the initial state
+ * @returns {{meta: {version: number}, data: any}}
+ */
+ generateInitialState (data) {
return {
meta: {
version: this.defaultVersion,
},
- data: initState,
+ data,
}
}
diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js
index 9b595d93c..25be6537b 100644
--- a/app/scripts/lib/nodeify.js
+++ b/app/scripts/lib/nodeify.js
@@ -1,6 +1,14 @@
const promiseToCallback = require('promise-to-callback')
const noop = function () {}
+/**
+ * A generator that returns a function which, when passed a promise, can treat that promise as a node style callback.
+ * The prime advantage being that callbacks are better for error handling.
+ *
+ * @param {Function} fn The function to handle as a callback
+ * @param {Object} context The context in which the fn is to be called, most often a this reference
+ *
+ */
module.exports = function nodeify (fn, context) {
return function () {
const args = [].slice.call(arguments)
diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js
deleted file mode 100644
index 5b1cd7f43..000000000
--- a/app/scripts/lib/nonce-tracker.js
+++ /dev/null
@@ -1,148 +0,0 @@
-const EthQuery = require('ethjs-query')
-const assert = require('assert')
-const Mutex = require('await-semaphore').Mutex
-
-class NonceTracker {
-
- constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
- this.provider = provider
- this.ethQuery = new EthQuery(provider)
- this.getPendingTransactions = getPendingTransactions
- this.getConfirmedTransactions = getConfirmedTransactions
- this.lockMap = {}
- }
-
- async getGlobalLock () {
- const globalMutex = this._lookupMutex('global')
- // await global mutex free
- const releaseLock = await globalMutex.acquire()
- return { releaseLock }
- }
-
- // releaseLock must be called
- // releaseLock must be called after adding signed tx to pending transactions (or discarding)
- async getNonceLock (address) {
- // await global mutex free
- await this._globalMutexFree()
- // await lock free, then take lock
- const releaseLock = await this._takeMutex(address)
- // evaluate multiple nextNonce strategies
- const nonceDetails = {}
- const networkNonceResult = await this._getNetworkNextNonce(address)
- const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
- const nextNetworkNonce = networkNonceResult.nonce
- const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
-
- const pendingTxs = this.getPendingTransactions(address)
- const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
-
- nonceDetails.params = {
- highestLocallyConfirmed,
- highestSuggested,
- nextNetworkNonce,
- }
- nonceDetails.local = localNonceResult
- nonceDetails.network = networkNonceResult
-
- const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
- assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
-
- // return nonce and release cb
- return { nextNonce, nonceDetails, releaseLock }
- }
-
- async _getCurrentBlock () {
- const blockTracker = this._getBlockTracker()
- const currentBlock = blockTracker.getCurrentBlock()
- if (currentBlock) return currentBlock
- return await new Promise((reject, resolve) => {
- blockTracker.once('latest', resolve)
- })
- }
-
- async _globalMutexFree () {
- const globalMutex = this._lookupMutex('global')
- const release = await globalMutex.acquire()
- release()
- }
-
- async _takeMutex (lockId) {
- const mutex = this._lookupMutex(lockId)
- const releaseLock = await mutex.acquire()
- return releaseLock
- }
-
- _lookupMutex (lockId) {
- let mutex = this.lockMap[lockId]
- if (!mutex) {
- mutex = new Mutex()
- this.lockMap[lockId] = mutex
- }
- return mutex
- }
-
- async _getNetworkNextNonce (address) {
- // calculate next nonce
- // we need to make sure our base count
- // and pending count are from the same block
- const currentBlock = await this._getCurrentBlock()
- const blockNumber = currentBlock.blockNumber
- const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
- const baseCount = baseCountBN.toNumber()
- assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
- const nonceDetails = { blockNumber, baseCount }
- return { name: 'network', nonce: baseCount, details: nonceDetails }
- }
-
- _getHighestLocallyConfirmed (address) {
- const confirmedTransactions = this.getConfirmedTransactions(address)
- const highest = this._getHighestNonce(confirmedTransactions)
- return Number.isInteger(highest) ? highest + 1 : 0
- }
-
- _reduceTxListToUniqueNonces (txList) {
- const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
- if (!index) return [txMeta]
- const nonceMatches = txList.filter((txData) => {
- return txMeta.txParams.nonce === txData.txParams.nonce
- })
- if (nonceMatches.length > 1) return reducedList
- reducedList.push(txMeta)
- return reducedList
- }, [])
- return reducedTxList
- }
-
- _getHighestNonce (txList) {
- const nonces = txList.map((txMeta) => {
- const nonce = txMeta.txParams.nonce
- assert(typeof nonce, 'string', 'nonces should be hex strings')
- return parseInt(nonce, 16)
- })
- const highestNonce = Math.max.apply(null, nonces)
- return highestNonce
- }
-
- _getHighestContinuousFrom (txList, startPoint) {
- const nonces = txList.map((txMeta) => {
- const nonce = txMeta.txParams.nonce
- assert(typeof nonce, 'string', 'nonces should be hex strings')
- return parseInt(nonce, 16)
- })
-
- let highest = startPoint
- while (nonces.includes(highest)) {
- highest++
- }
-
- return { name: 'local', nonce: highest, details: { startPoint, highest } }
- }
-
- // this is a hotfix for the fact that the blockTracker will
- // change when the network changes
- _getBlockTracker () {
- return this.provider._blockTracker
- }
-}
-
-module.exports = NonceTracker
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/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
deleted file mode 100644
index e8869e6b8..000000000
--- a/app/scripts/lib/pending-tx-tracker.js
+++ /dev/null
@@ -1,189 +0,0 @@
-const EventEmitter = require('events')
-const EthQuery = require('ethjs-query')
-/*
-
- Utility class for tracking the transactions as they
- go from a pending state to a confirmed (mined in a block) state
-
- As well as continues broadcast while in the pending state
-
- ~config is not optional~
- requires a: {
- provider: //,
- nonceTracker: //see nonce tracker,
- getPendingTransactions: //() a function for getting an array of transactions,
- publishTransaction: //(rawTx) a async function for publishing raw transactions,
- }
-
-*/
-
-module.exports = class PendingTransactionTracker extends EventEmitter {
- constructor (config) {
- super()
- this.query = new EthQuery(config.provider)
- this.nonceTracker = config.nonceTracker
- // default is one day
- this.getPendingTransactions = config.getPendingTransactions
- this.getCompletedTransactions = config.getCompletedTransactions
- this.publishTransaction = config.publishTransaction
- this._checkPendingTxs()
- }
-
- // checks if a signed tx is in a block and
- // if included sets the tx status as 'confirmed'
- checkForTxInBlock (block) {
- const signedTxList = this.getPendingTransactions()
- if (!signedTxList.length) return
- signedTxList.forEach((txMeta) => {
- const txHash = txMeta.hash
- const txId = txMeta.id
-
- if (!txHash) {
- const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
- noTxHashErr.name = 'NoTxHashError'
- this.emit('tx:failed', txId, noTxHashErr)
- return
- }
-
-
- block.transactions.forEach((tx) => {
- if (tx.hash === txHash) this.emit('tx:confirmed', txId)
- })
- })
- }
-
- queryPendingTxs ({ oldBlock, newBlock }) {
- // check pending transactions on start
- if (!oldBlock) {
- this._checkPendingTxs()
- return
- }
- // if we synced by more than one block, check for missed pending transactions
- const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16)
- if (diff > 1) this._checkPendingTxs()
- }
-
-
- resubmitPendingTxs (block) {
- const pending = this.getPendingTransactions()
- // only try resubmitting if their are transactions to resubmit
- if (!pending.length) return
- pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => {
- /*
- Dont marked as failed if the error is a "known" transaction warning
- "there is already a transaction with the same sender-nonce
- but higher/same gas price"
-
- Also don't mark as failed if it has ever been broadcast successfully.
- A successful broadcast means it may still be mined.
- */
- const errorMessage = err.message.toLowerCase()
- const isKnownTx = (
- // geth
- errorMessage.includes('replacement transaction underpriced') ||
- errorMessage.includes('known transaction') ||
- // parity
- errorMessage.includes('gas price too low to replace') ||
- errorMessage.includes('transaction with the same hash was already imported') ||
- // other
- errorMessage.includes('gateway timeout') ||
- errorMessage.includes('nonce too low')
- )
- // ignore resubmit warnings, return early
- if (isKnownTx) return
- // encountered real error - transition to error state
- txMeta.warning = {
- error: errorMessage,
- message: 'There was an error when resubmitting this transaction.',
- }
- this.emit('tx:warning', txMeta, err)
- }))
- }
-
- async _resubmitTx (txMeta, latestBlockNumber) {
- if (!txMeta.firstRetryBlockNumber) {
- this.emit('tx:block-update', txMeta, latestBlockNumber)
- }
-
- const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
- const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
-
- const retryCount = txMeta.retryCount || 0
-
- // Exponential backoff to limit retries at publishing
- if (txBlockDistance <= Math.pow(2, retryCount) - 1) return
-
- // Only auto-submit already-signed txs:
- if (!('rawTx' in txMeta)) return
-
- const rawTx = txMeta.rawTx
- const txHash = await this.publishTransaction(rawTx)
-
- // Increment successful tries:
- this.emit('tx:retry', txMeta)
- return txHash
- }
-
- async _checkPendingTx (txMeta) {
- const txHash = txMeta.hash
- const txId = txMeta.id
-
- // extra check in case there was an uncaught error during the
- // signature and submission process
- if (!txHash) {
- const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
- noTxHashErr.name = 'NoTxHashError'
- this.emit('tx:failed', txId, noTxHashErr)
- return
- }
-
- // If another tx with the same nonce is mined, set as failed.
- const taken = await this._checkIfNonceIsTaken(txMeta)
- if (taken) {
- const nonceTakenErr = new Error('Another transaction with this nonce has been mined.')
- nonceTakenErr.name = 'NonceTakenErr'
- return this.emit('tx:failed', txId, nonceTakenErr)
- }
-
- // get latest transaction status
- let txParams
- try {
- txParams = await this.query.getTransactionByHash(txHash)
- if (!txParams) return
- if (txParams.blockNumber) {
- this.emit('tx:confirmed', txId)
- }
- } catch (err) {
- txMeta.warning = {
- error: err.message,
- message: 'There was a problem loading this transaction.',
- }
- this.emit('tx:warning', txMeta, err)
- }
- }
-
- // checks the network for signed txs and
- // if confirmed sets the tx status as 'confirmed'
- async _checkPendingTxs () {
- const signedTxList = this.getPendingTransactions()
- // in order to keep the nonceTracker accurate we block it while updating pending transactions
- const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
- try {
- await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
- } catch (err) {
- console.error('PendingTransactionWatcher - Error updating pending transactions')
- console.error(err)
- }
- nonceGlobalLock.releaseLock()
- }
-
- async _checkIfNonceIsTaken (txMeta) {
- const address = txMeta.txParams.from
- const completed = this.getCompletedTransactions(address)
- const sameNonce = completed.filter((otherMeta) => {
- return otherMeta.txParams.nonce === txMeta.txParams.nonce
- })
- return sameNonce.length > 0
- }
-
-}
diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js
index 6602f5aa8..e96ced1f2 100644
--- a/app/scripts/lib/personal-message-manager.js
+++ b/app/scripts/lib/personal-message-manager.js
@@ -3,9 +3,39 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const createId = require('./random-id')
const hexRe = /^[0-9A-Fa-f]+$/g
+const log = require('loglevel')
+/**
+ * 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({
@@ -15,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)
@@ -44,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
@@ -69,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}".')
@@ -93,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) {
@@ -101,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
@@ -108,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 9cea22029..3b5afb800 100644
--- a/app/scripts/lib/seed-phrase-verifier.js
+++ b/app/scripts/lib/seed-phrase-verifier.js
@@ -1,12 +1,21 @@
const KeyringController = require('eth-keyring-controller')
+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/setupRaven.js b/app/scripts/lib/setupRaven.js
index 9ec9a256f..b1b67f771 100644
--- a/app/scripts/lib/setupRaven.js
+++ b/app/scripts/lib/setupRaven.js
@@ -23,22 +23,16 @@ function setupRaven(opts) {
release,
transport: function(opts) {
const report = opts.data
- // simplify certain complex error messages
- report.exception.values.forEach(item => {
- let errorMessage = item.value
- // simplify ethjs error messages
- errorMessage = extractEthjsErrorMessage(errorMessage)
- // simplify 'Transaction Failed: known transaction'
- if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
- // cut the hash from the error message
- errorMessage = 'Transaction Failed: known transaction'
- }
- // finalize
- item.value = errorMessage
- })
-
- // modify report urls
- rewriteReportUrls(report)
+ try {
+ // handle error-like non-error exceptions
+ nonErrorException(report)
+ // simplify certain complex error messages (e.g. Ethjs)
+ simplifyErrorMessages(report)
+ // modify report urls
+ rewriteReportUrls(report)
+ } catch (err) {
+ console.warn(err)
+ }
// make request normally
client._makeRequest(opts)
},
@@ -48,15 +42,42 @@ function setupRaven(opts) {
return Raven
}
+function nonErrorException(report) {
+ // handle errors that lost their error-ness in serialization
+ if (report.message.includes('Non-Error exception captured with keys: message')) {
+ if (!(report.extra && report.extra.__serialized__)) return
+ report.message = `Non-Error Exception: ${report.extra.__serialized__.message}`
+ }
+}
+
+function simplifyErrorMessages(report) {
+ if (report.exception && report.exception.values) {
+ report.exception.values.forEach(item => {
+ let errorMessage = item.value
+ // simplify ethjs error messages
+ errorMessage = extractEthjsErrorMessage(errorMessage)
+ // simplify 'Transaction Failed: known transaction'
+ if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
+ // cut the hash from the error message
+ errorMessage = 'Transaction Failed: known transaction'
+ }
+ // finalize
+ item.value = errorMessage
+ })
+ }
+}
+
function rewriteReportUrls(report) {
// update request url
report.request.url = toMetamaskUrl(report.request.url)
// update exception stack trace
- report.exception.values.forEach(item => {
- item.stacktrace.frames.forEach(frame => {
- frame.filename = toMetamaskUrl(frame.filename)
+ if (report.exception && report.exception.values) {
+ report.exception.values.forEach(item => {
+ item.stacktrace.frames.forEach(frame => {
+ frame.filename = toMetamaskUrl(frame.filename)
+ })
})
- })
+ }
}
function toMetamaskUrl(origUrl) {
diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js
index 8bb0b4f3c..3dbc064b5 100644
--- a/app/scripts/lib/stream-utils.js
+++ b/app/scripts/lib/stream-utils.js
@@ -8,20 +8,34 @@ module.exports = {
setupMultiplex: setupMultiplex,
}
+/**
+ * Returns a stream transform that parses JSON strings passing through
+ * @return {stream.Transform}
+ */
function jsonParseStream () {
- return Through.obj(function (serialized, encoding, cb) {
+ return Through.obj(function (serialized, _, cb) {
this.push(JSON.parse(serialized))
cb()
})
}
+/**
+ * Returns a stream transform that calls {@code JSON.stringify}
+ * on objects passing through
+ * @return {stream.Transform} the stream transform
+ */
function jsonStringifyStream () {
- return Through.obj(function (obj, encoding, cb) {
+ return Through.obj(function (obj, _, cb) {
this.push(JSON.stringify(obj))
cb()
})
}
+/**
+ * Sets up stream multiplexing for the given stream
+ * @param {any} connectionStream - the stream to mux
+ * @return {stream.Stream} the multiplexed stream
+ */
function setupMultiplex (connectionStream) {
const mux = new ObjectMultiplex()
pump(
diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/lib/tx-gas-utils.js
deleted file mode 100644
index 829b4c421..000000000
--- a/app/scripts/lib/tx-gas-utils.js
+++ /dev/null
@@ -1,136 +0,0 @@
-const EthQuery = require('ethjs-query')
-const {
- hexToBn,
- BnMultiplyByFraction,
- bnToHex,
-} = require('./util')
-const { addHexPrefix, isValidAddress } = require('ethereumjs-util')
-const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
-
-/*
-tx-utils are utility methods for Transaction manager
-its passed ethquery
-and used to do things like calculate gas of a tx.
-*/
-
-module.exports = class TxGasUtil {
-
- constructor (provider) {
- this.query = new EthQuery(provider)
- }
-
- async analyzeGasUsage (txMeta) {
- const block = await this.query.getBlockByNumber('latest', true)
- let estimatedGasHex
- try {
- estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
- } catch (err) {
- const simulationFailed = (
- err.message.includes('Transaction execution error.') ||
- err.message.includes('gas required exceeds allowance or always failing transaction')
- )
- if (simulationFailed) {
- txMeta.simulationFails = true
- return txMeta
- }
- }
- this.setTxGas(txMeta, block.gasLimit, estimatedGasHex)
- return txMeta
- }
-
- async estimateTxGas (txMeta, blockGasLimitHex) {
- const txParams = txMeta.txParams
-
- // check if gasLimit is already specified
- txMeta.gasLimitSpecified = Boolean(txParams.gas)
-
- // if it is, use that value
- if (txMeta.gasLimitSpecified) {
- return txParams.gas
- }
-
- // if recipient has no code, gas is 21k max:
- const recipient = txParams.to
- const hasRecipient = Boolean(recipient)
- let code
- if (recipient) code = await this.query.getCode(recipient)
-
- if (hasRecipient && (!code || code === '0x')) {
- txParams.gas = SIMPLE_GAS_COST
- txMeta.simpleSend = true // Prevents buffer addition
- return SIMPLE_GAS_COST
- }
-
- // if not, fall back to block gasLimit
- const blockGasLimitBN = hexToBn(blockGasLimitHex)
- const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
- txParams.gas = bnToHex(saferGasLimitBN)
-
- // run tx
- return await this.query.estimateGas(txParams)
- }
-
- setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
- txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
- const txParams = txMeta.txParams
-
- // if gasLimit was specified and doesnt OOG,
- // use original specified amount
- if (txMeta.gasLimitSpecified || txMeta.simpleSend) {
- txMeta.estimatedGas = txParams.gas
- return
- }
- // if gasLimit not originally specified,
- // try adding an additional gas buffer to our estimation for safety
- const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex)
- txParams.gas = recommendedGasHex
- return
- }
-
- addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
- const initialGasLimitBn = hexToBn(initialGasLimitHex)
- const blockGasLimitBn = hexToBn(blockGasLimitHex)
- const upperGasLimitBn = blockGasLimitBn.muln(0.9)
- const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)
-
- // if initialGasLimit is above blockGasLimit, dont modify it
- if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn)
- // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
- if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn)
- // otherwise use blockGasLimit
- return bnToHex(upperGasLimitBn)
- }
-
- async validateTxParams (txParams) {
- this.validateFrom(txParams)
- this.validateRecipient(txParams)
- if ('value' in txParams) {
- const value = txParams.value.toString()
- if (value.includes('-')) {
- throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
- }
-
- if (value.includes('.')) {
- throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
- }
- }
- }
-
- validateFrom (txParams) {
- if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
- if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
- }
-
- validateRecipient (txParams) {
- if (txParams.to === '0x' || txParams.to === null ) {
- if (txParams.data) {
- delete txParams.to
- } else {
- throw new Error('Invalid recipient address')
- }
- } else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) {
- throw new Error('Invalid recipient address')
- }
- return txParams
- }
-} \ No newline at end of file
diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js
deleted file mode 100644
index 94c7b6792..000000000
--- a/app/scripts/lib/tx-state-history-helper.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const jsonDiffer = require('fast-json-patch')
-const clone = require('clone')
-
-module.exports = {
- generateHistoryEntry,
- replayHistory,
- snapshotFromTxMeta,
- migrateFromSnapshotsToDiffs,
-}
-
-
-function migrateFromSnapshotsToDiffs (longHistory) {
- return (
- longHistory
- // convert non-initial history entries into diffs
- .map((entry, index) => {
- if (index === 0) return entry
- return generateHistoryEntry(longHistory[index - 1], entry)
- })
- )
-}
-
-function generateHistoryEntry (previousState, newState, note) {
- const entry = jsonDiffer.compare(previousState, newState)
- // Add a note to the first op, since it breaks if we append it to the entry
- if (note && entry[0]) entry[0].note = note
- return entry
-}
-
-function replayHistory (_shortHistory) {
- const shortHistory = clone(_shortHistory)
- return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
-}
-
-function snapshotFromTxMeta (txMeta) {
- // create txMeta snapshot for history
- const snapshot = clone(txMeta)
- // dont include previous history in this snapshot
- delete snapshot.history
- return snapshot
-}
diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js
deleted file mode 100644
index 2ab24d6a0..000000000
--- a/app/scripts/lib/tx-state-manager.js
+++ /dev/null
@@ -1,297 +0,0 @@
-const extend = require('xtend')
-const EventEmitter = require('events')
-const ObservableStore = require('obs-store')
-const createId = require('./random-id')
-const ethUtil = require('ethereumjs-util')
-const txStateHistoryHelper = require('./tx-state-history-helper')
-
-// STATUS METHODS
- // statuses:
- // - `'unapproved'` the user has not responded
- // - `'rejected'` the user has responded no!
- // - `'approved'` the user has approved the tx
- // - `'signed'` the tx is signed
- // - `'submitted'` the tx is sent to a server
- // - `'confirmed'` the tx has been included in a block.
- // - `'failed'` the tx failed for some reason, included on tx data.
- // - `'dropped'` the tx nonce was already used
-
-module.exports = class TransactionStateManager extends EventEmitter {
- constructor ({ initState, txHistoryLimit, getNetwork }) {
- super()
-
- this.store = new ObservableStore(
- extend({
- transactions: [],
- }, initState))
- this.txHistoryLimit = txHistoryLimit
- this.getNetwork = getNetwork
- }
-
- generateTxMeta (opts) {
- return extend({
- id: createId(),
- time: (new Date()).getTime(),
- status: 'unapproved',
- metamaskNetworkId: this.getNetwork(),
- loadingDefaults: true,
- }, opts)
- }
-
- getTxList () {
- const network = this.getNetwork()
- const fullTxList = this.getFullTxList()
- return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
- }
-
- getFullTxList () {
- return this.store.getState().transactions
- }
-
- // Returns the tx list
- getUnapprovedTxList () {
- const txList = this.getTxsByMetaData('status', 'unapproved')
- return txList.reduce((result, tx) => {
- result[tx.id] = tx
- return result
- }, {})
- }
-
- getPendingTransactions (address) {
- const opts = { status: 'submitted' }
- if (address) opts.from = address
- return this.getFilteredTxList(opts)
- }
-
- getConfirmedTransactions (address) {
- const opts = { status: 'confirmed' }
- if (address) opts.from = address
- return this.getFilteredTxList(opts)
- }
-
- addTx (txMeta) {
- this.once(`${txMeta.id}:signed`, function (txId) {
- this.removeAllListeners(`${txMeta.id}:rejected`)
- })
- this.once(`${txMeta.id}:rejected`, function (txId) {
- this.removeAllListeners(`${txMeta.id}:signed`)
- })
- // initialize history
- txMeta.history = []
- // capture initial snapshot of txMeta for history
- const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
- txMeta.history.push(snapshot)
-
- const transactions = this.getFullTxList()
- const txCount = transactions.length
- const txHistoryLimit = this.txHistoryLimit
-
- // checks if the length of the tx history is
- // longer then desired persistence limit
- // and then if it is removes only confirmed
- // or rejected tx's.
- // not tx's that are pending or unapproved
- if (txCount > txHistoryLimit - 1) {
- const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
- transactions.splice(index, 1)
- }
- transactions.push(txMeta)
- this._saveTxList(transactions)
- return txMeta
- }
- // gets tx by Id and returns it
- getTx (txId) {
- const txMeta = this.getTxsByMetaData('id', txId)[0]
- return txMeta
- }
-
- updateTx (txMeta, note) {
- // validate txParams
- if (txMeta.txParams) {
- this.validateTxParams(txMeta.txParams)
- }
-
- // create txMeta snapshot for history
- const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
- // recover previous tx state obj
- const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
- // generate history entry and add to history
- const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note)
- txMeta.history.push(entry)
-
- // commit txMeta to state
- const txId = txMeta.id
- const txList = this.getFullTxList()
- const index = txList.findIndex(txData => txData.id === txId)
- txList[index] = txMeta
- this._saveTxList(txList)
- }
-
-
- // merges txParams obj onto txData.txParams
- // use extend to ensure that all fields are filled
- updateTxParams (txId, txParams) {
- const txMeta = this.getTx(txId)
- txMeta.txParams = extend(txMeta.txParams, txParams)
- this.updateTx(txMeta, `txStateManager#updateTxParams`)
- }
-
- // validates txParams members by type
- validateTxParams(txParams) {
- Object.keys(txParams).forEach((key) => {
- const value = txParams[key]
- // validate types
- switch (key) {
- case 'chainId':
- if (typeof value !== 'number' && typeof value !== 'string') throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`)
- break
- default:
- if (typeof value !== 'string') throw new Error(`${key} in txParams is not a string. got: (${value})`)
- if (!ethUtil.isHexPrefixed(value)) throw new Error(`${key} in txParams is not hex prefixed. got: (${value})`)
- break
- }
- })
- }
-
-/*
- Takes an object of fields to search for eg:
- let thingsToLookFor = {
- to: '0x0..',
- from: '0x0..',
- status: 'signed',
- err: undefined,
- }
- and returns a list of tx with all
- options matching
-
- ****************HINT****************
- | `err: undefined` is like looking |
- | for a tx with no err |
- | so you can also search txs that |
- | dont have something as well by |
- | setting the value as undefined |
- ************************************
-
- this is for things like filtering a the tx list
- for only tx's from 1 account
- or for filltering for all txs from one account
- and that have been 'confirmed'
- */
- getFilteredTxList (opts, initialList) {
- let filteredTxList = initialList
- Object.keys(opts).forEach((key) => {
- filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
- })
- return filteredTxList
- }
-
- getTxsByMetaData (key, value, txList = this.getTxList()) {
- return txList.filter((txMeta) => {
- if (txMeta.txParams[key]) {
- return txMeta.txParams[key] === value
- } else {
- return txMeta[key] === value
- }
- })
- }
-
- // get::set status
-
- // should return the status of the tx.
- getTxStatus (txId) {
- const txMeta = this.getTx(txId)
- return txMeta.status
- }
-
- // should update the status of the tx to 'rejected'.
- setTxStatusRejected (txId) {
- this._setTxStatus(txId, 'rejected')
- }
-
- // should update the status of the tx to 'unapproved'.
- setTxStatusUnapproved (txId) {
- this._setTxStatus(txId, 'unapproved')
- }
- // should update the status of the tx to 'approved'.
- setTxStatusApproved (txId) {
- this._setTxStatus(txId, 'approved')
- }
-
- // should update the status of the tx to 'signed'.
- setTxStatusSigned (txId) {
- this._setTxStatus(txId, 'signed')
- }
-
- // should update the status of the tx to 'submitted'.
- // and add a time stamp for when it was called
- setTxStatusSubmitted (txId) {
- const txMeta = this.getTx(txId)
- txMeta.submittedTime = (new Date()).getTime()
- this.updateTx(txMeta, 'txStateManager - add submitted time stamp')
- this._setTxStatus(txId, 'submitted')
- }
-
- // should update the status of the tx to 'confirmed'.
- setTxStatusConfirmed (txId) {
- this._setTxStatus(txId, 'confirmed')
- }
-
- // should update the status dropped
- setTxStatusDropped (txId) {
- this._setTxStatus(txId, 'dropped')
- }
-
-
- setTxStatusFailed (txId, err) {
- const txMeta = this.getTx(txId)
- txMeta.err = {
- message: err.toString(),
- stack: err.stack,
- }
- this.updateTx(txMeta)
- this._setTxStatus(txId, 'failed')
- }
-
- wipeTransactions (address) {
- // network only tx
- const txs = this.getFullTxList()
- const network = this.getNetwork()
-
- // Filter out the ones from the current account and network
- const otherAccountTxs = txs.filter((txMeta) => !(txMeta.txParams.from === address && txMeta.metamaskNetworkId === network))
-
- // Update state
- this._saveTxList(otherAccountTxs)
- }
-//
-// PRIVATE METHODS
-//
-
- // Should find the tx in the tx list and
- // update it.
- // should set the status in txData
- // - `'unapproved'` the user has not responded
- // - `'rejected'` the user has responded no!
- // - `'approved'` the user has approved the tx
- // - `'signed'` the tx is signed
- // - `'submitted'` the tx is sent to a server
- // - `'confirmed'` the tx has been included in a block.
- // - `'failed'` the tx failed for some reason, included on tx data.
- _setTxStatus (txId, status) {
- const txMeta = this.getTx(txId)
- txMeta.status = status
- this.emit(`${txMeta.id}:${status}`, txId)
- this.emit(`tx:status-update`, txId, status)
- if (['submitted', 'rejected', 'failed'].includes(status)) {
- this.emit(`${txMeta.id}:finished`, txMeta)
- }
- this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
- this.emit('update:badge')
- }
-
- // Saves the new/updated txList.
- // Function is intended only for internal use
- _saveTxList (transactions) {
- this.store.updateState({ transactions })
- }
-}
diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js
index 8b760790e..c58921610 100644
--- a/app/scripts/lib/typed-message-manager.js
+++ b/app/scripts/lib/typed-message-manager.js
@@ -3,9 +3,38 @@ const ObservableStore = require('obs-store')
const createId = require('./random-id')
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/lib/util.js b/app/scripts/lib/util.js
index 6dee9edf0..431d1e59c 100644
--- a/app/scripts/lib/util.js
+++ b/app/scripts/lib/util.js
@@ -1,20 +1,53 @@
const ethUtil = require('ethereumjs-util')
const assert = require('assert')
const BN = require('bn.js')
+const {
+ ENVIRONMENT_TYPE_POPUP,
+ ENVIRONMENT_TYPE_NOTIFICATION,
+ ENVIRONMENT_TYPE_FULLSCREEN,
+} = require('./enums')
-module.exports = {
- getStack,
- sufficientBalance,
- hexToBn,
- bnToHex,
- BnMultiplyByFraction,
-}
-
+/**
+ * Generates an example stack trace
+ *
+ * @returns {string} A stack trace
+ *
+ */
function getStack () {
const stack = new Error('Stack trace generator - not an error').stack
return stack
}
+/**
+ * Used to determine the window type through which the app is being viewed.
+ * - 'popup' refers to the extension opened through the browser app icon (in top right corner in chrome and firefox)
+ * - 'responsive' refers to the main browser window
+ * - 'notification' refers to the popup that appears in its own window when taking action outside of metamask
+ *
+ * @returns {string} A single word label that represents the type of window through which the app is being viewed
+ *
+ */
+const getEnvironmentType = (url = window.location.href) => {
+ if (url.match(/popup.html(?:\?.+)*$/)) {
+ return ENVIRONMENT_TYPE_POPUP
+ } else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) {
+ return ENVIRONMENT_TYPE_FULLSCREEN
+ } else {
+ return ENVIRONMENT_TYPE_NOTIFICATION
+ }
+}
+
+/**
+ * Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee
+ *
+ * @param {object} txParams Contains data about a transaction
+ * @param {string} txParams.gas The gas for a transaction
+ * @param {string} txParams.gasPrice The price per gas for the transaction
+ * @param {string} txParams.value The value of ETH to send
+ * @param {string} hexBalance A balance of ETH represented as a hex string
+ * @returns {boolean} Whether the balance is greater than or equal to the value plus the value of gas times gasPrice
+ *
+ */
function sufficientBalance (txParams, hexBalance) {
// validate hexBalance is a hex string
assert.equal(typeof hexBalance, 'string', 'sufficientBalance - hexBalance is not a hex string')
@@ -29,16 +62,48 @@ function sufficientBalance (txParams, hexBalance) {
return balance.gte(maxCost)
}
+/**
+ * Converts a BN object to a hex string with a '0x' prefix
+ *
+ * @param {BN} inputBn The BN to convert to a hex string
+ * @returns {string} A '0x' prefixed hex string
+ *
+ */
function bnToHex (inputBn) {
return ethUtil.addHexPrefix(inputBn.toString(16))
}
+/**
+ * Converts a hex string to a BN object
+ *
+ * @param {string} inputHex A number represented as a hex string
+ * @returns {Object} A BN object
+ *
+ */
function hexToBn (inputHex) {
return new BN(ethUtil.stripHexPrefix(inputHex), 16)
}
+/**
+ * Used to multiply a BN by a fraction
+ *
+ * @param {BN} targetBN The number to multiply by a fraction
+ * @param {number|string} numerator The numerator of the fraction multiplier
+ * @param {number|string} denominator The denominator of the fraction multiplier
+ * @returns {BN} The product of the multiplication
+ *
+ */
function BnMultiplyByFraction (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}
+
+module.exports = {
+ getStack,
+ getEnvironmentType,
+ sufficientBalance,
+ hexToBn,
+ bnToHex,
+ BnMultiplyByFraction,
+}