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

Merge pull request #4279 from MetaMask/network-remove-provider-engine

Enhancement: New BlockTracker and Json-Rpc-Engine based Provider
This commit is contained in:
Dan Finlay 2018-08-15 15:41:05 -07:00 committed by GitHub
commit 955ec2dca6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1282 additions and 2915 deletions

View File

@ -1,4 +1,4 @@
{ {
"presets": [["env", { "debug": true }], "react", "stage-0"], "presets": [["env"], "react", "stage-0"],
"plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"] "plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
} }

2
.gitignore vendored
View File

@ -9,6 +9,7 @@ package
# IDEs # IDEs
.idea .idea
.vscode .vscode
.sublime-project
# VIM # VIM
*.swp *.swp
@ -34,6 +35,7 @@ test/bundle.js
test/test-bundle.js test/test-bundle.js
test-artifacts test-artifacts
test-builds
#ignore css output and sourcemaps #ignore css output and sourcemaps
ui/app/css/output/ ui/app/css/output/

View File

@ -19,7 +19,7 @@ const PortStream = require('./lib/port-stream.js')
const createStreamSink = require('./lib/createStreamSink') const createStreamSink = require('./lib/createStreamSink')
const NotificationManager = require('./lib/notification-manager.js') const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller') const MetamaskController = require('./metamask-controller')
const firstTimeState = require('./first-time-state') const rawFirstTimeState = require('./first-time-state')
const setupRaven = require('./lib/setupRaven') const setupRaven = require('./lib/setupRaven')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
@ -34,6 +34,9 @@ const {
ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_FULLSCREEN,
} = require('./lib/enums') } = require('./lib/enums')
// METAMASK_TEST_CONFIG is used in e2e tests to set the default network to localhost
const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG)
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG const METAMASK_DEBUG = process.env.METAMASK_DEBUG

View File

@ -80,7 +80,7 @@ class BalanceController {
} }
}) })
this.accountTracker.store.subscribe(update) this.accountTracker.store.subscribe(update)
this.blockTracker.on('block', update) this.blockTracker.on('latest', update)
} }
/** /**

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const extend = require('xtend') const extend = require('xtend')
const log = require('loglevel') const log = require('loglevel')

View File

@ -0,0 +1,25 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createBlockReEmitMiddleware = require('eth-json-rpc-middleware/block-reemit')
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
const createInfuraMiddleware = require('eth-json-rpc-infura')
const BlockTracker = require('eth-block-tracker')
module.exports = createInfuraClient
function createInfuraClient ({ network }) {
const infuraMiddleware = createInfuraMiddleware({ network })
const blockProvider = providerFromMiddleware(infuraMiddleware)
const blockTracker = new BlockTracker({ provider: blockProvider })
const networkMiddleware = mergeMiddleware([
createBlockCacheMiddleware({ blockTracker }),
createInflightMiddleware(),
createBlockReEmitMiddleware({ blockTracker, provider: blockProvider }),
createBlockTrackerInspectorMiddleware({ blockTracker }),
infuraMiddleware,
])
return { networkMiddleware, blockTracker }
}

View File

@ -0,0 +1,25 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref')
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
const BlockTracker = require('eth-block-tracker')
module.exports = createJsonRpcClient
function createJsonRpcClient ({ rpcUrl }) {
const fetchMiddleware = createFetchMiddleware({ rpcUrl })
const blockProvider = providerFromMiddleware(fetchMiddleware)
const blockTracker = new BlockTracker({ provider: blockProvider })
const networkMiddleware = mergeMiddleware([
createBlockRefMiddleware({ blockTracker }),
createBlockCacheMiddleware({ blockTracker }),
createInflightMiddleware(),
createBlockTrackerInspectorMiddleware({ blockTracker }),
fetchMiddleware,
])
return { networkMiddleware, blockTracker }
}

View File

@ -0,0 +1,21 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref')
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
const BlockTracker = require('eth-block-tracker')
module.exports = createLocalhostClient
function createLocalhostClient () {
const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' })
const blockProvider = providerFromMiddleware(fetchMiddleware)
const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 })
const networkMiddleware = mergeMiddleware([
createBlockRefMiddleware({ blockTracker }),
createBlockTrackerInspectorMiddleware({ blockTracker }),
fetchMiddleware,
])
return { networkMiddleware, blockTracker }
}

View File

@ -0,0 +1,43 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
const createWalletSubprovider = require('eth-json-rpc-middleware/wallet')
module.exports = createMetamaskMiddleware
function createMetamaskMiddleware ({
version,
getAccounts,
processTransaction,
processEthSignMessage,
processTypedMessage,
processPersonalMessage,
getPendingNonce,
}) {
const metamaskMiddleware = mergeMiddleware([
createScaffoldMiddleware({
// staticSubprovider
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
}),
createWalletSubprovider({
getAccounts,
processTransaction,
processEthSignMessage,
processTypedMessage,
processPersonalMessage,
}),
createPendingNonceMiddleware({ getPendingNonce }),
])
return metamaskMiddleware
}
function createPendingNonceMiddleware ({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => {
if (req.method !== 'eth_getTransactionCount') return next()
const address = req.params[0]
const blockRef = req.params[1]
if (blockRef !== 'pending') return next()
req.result = await getPendingNonce(address)
})
}

View File

@ -1,15 +1,17 @@
const assert = require('assert') const assert = require('assert')
const EventEmitter = require('events') const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.js')
const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js')
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed') const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const createEventEmitterProxy = require('../../lib/events-proxy.js') const JsonRpcEngine = require('json-rpc-engine')
const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine')
const log = require('loglevel') const log = require('loglevel')
const urlUtil = require('url') const createMetamaskMiddleware = require('./createMetamaskMiddleware')
const createInfuraClient = require('./createInfuraClient')
const createJsonRpcClient = require('./createJsonRpcClient')
const createLocalhostClient = require('./createLocalhostClient')
const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy')
const { const {
ROPSTEN, ROPSTEN,
RINKEBY, RINKEBY,
@ -17,7 +19,6 @@ const {
MAINNET, MAINNET,
LOCALHOST, LOCALHOST,
} = require('./enums') } = require('./enums')
const LOCALHOST_RPC_URL = 'http://localhost:8545'
const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET]
const env = process.env.METAMASK_ENV const env = process.env.METAMASK_ENV
@ -39,21 +40,27 @@ module.exports = class NetworkController extends EventEmitter {
this.providerStore = new ObservableStore(providerConfig) this.providerStore = new ObservableStore(providerConfig)
this.networkStore = new ObservableStore('loading') this.networkStore = new ObservableStore('loading')
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
// create event emitter proxy
this._proxy = createEventEmitterProxy()
this.on('networkDidChange', this.lookupNetwork) this.on('networkDidChange', this.lookupNetwork)
// provider and block tracker
this._provider = null
this._blockTracker = null
// provider and block tracker proxies - because the network changes
this._providerProxy = null
this._blockTrackerProxy = null
} }
initializeProvider (_providerParams) { initializeProvider (providerParams) {
this._baseProviderParams = _providerParams this._baseProviderParams = providerParams
const { type, rpcTarget } = this.providerStore.getState() const { type, rpcTarget } = this.providerStore.getState()
this._configureProvider({ type, rpcTarget }) this._configureProvider({ type, rpcTarget })
this._proxy.on('block', this._logBlock.bind(this))
this._proxy.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this._proxy)
this.lookupNetwork() this.lookupNetwork()
return this._proxy }
// return the proxies so the references will always be good
getProviderAndBlockTracker () {
const provider = this._providerProxy
const blockTracker = this._blockTrackerProxy
return { provider, blockTracker }
} }
verifyNetwork () { verifyNetwork () {
@ -75,10 +82,11 @@ module.exports = class NetworkController extends EventEmitter {
lookupNetwork () { lookupNetwork () {
// Prevent firing when provider is not defined. // Prevent firing when provider is not defined.
if (!this.ethQuery || !this.ethQuery.sendAsync) { if (!this._provider) {
return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery') return log.warn('NetworkController - lookupNetwork aborted due to missing provider')
} }
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { const ethQuery = new EthQuery(this._provider)
ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading') if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network) log.info('web3.getNetwork returned ' + network)
this.setNetworkState(network) this.setNetworkState(network)
@ -131,7 +139,7 @@ module.exports = class NetworkController extends EventEmitter {
this._configureInfuraProvider(opts) this._configureInfuraProvider(opts)
// other type-based rpc endpoints // other type-based rpc endpoints
} else if (type === LOCALHOST) { } else if (type === LOCALHOST) {
this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL }) this._configureLocalhostProvider()
// url-based rpc endpoints // url-based rpc endpoints
} else if (type === 'rpc') { } else if (type === 'rpc') {
this._configureStandardProvider({ rpcUrl: rpcTarget }) this._configureStandardProvider({ rpcUrl: rpcTarget })
@ -141,49 +149,47 @@ module.exports = class NetworkController extends EventEmitter {
} }
_configureInfuraProvider ({ type }) { _configureInfuraProvider ({ type }) {
log.info('_configureInfuraProvider', type) log.info('NetworkController - configureInfuraProvider', type)
const infuraProvider = createInfuraProvider({ network: type }) const networkClient = createInfuraClient({ network: type })
const infuraSubprovider = new SubproviderFromProvider(infuraProvider) this._setNetworkClient(networkClient)
const providerParams = extend(this._baseProviderParams, { }
engineParams: {
pollingInterval: 8000, _configureLocalhostProvider () {
blockTrackerProvider: infuraProvider, log.info('NetworkController - configureLocalhostProvider')
}, const networkClient = createLocalhostClient()
dataSubprovider: infuraSubprovider, this._setNetworkClient(networkClient)
})
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
} }
_configureStandardProvider ({ rpcUrl }) { _configureStandardProvider ({ rpcUrl }) {
// urlUtil handles malformed urls log.info('NetworkController - configureStandardProvider', rpcUrl)
rpcUrl = urlUtil.parse(rpcUrl).format() const networkClient = createJsonRpcClient({ rpcUrl })
const providerParams = extend(this._baseProviderParams, { this._setNetworkClient(networkClient)
rpcUrl,
engineParams: {
pollingInterval: 8000,
},
})
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
} }
_setProvider (provider) { _setNetworkClient ({ networkMiddleware, blockTracker }) {
// collect old block tracker events const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams)
const oldProvider = this._provider const engine = new JsonRpcEngine()
let blockTrackerHandlers engine.push(metamaskMiddleware)
if (oldProvider) { engine.push(networkMiddleware)
// capture old block handlers const provider = providerFromEngine(engine)
blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers this._setProviderAndBlockTracker({ provider, blockTracker })
// tear down }
oldProvider.removeAllListeners()
oldProvider.stop() _setProviderAndBlockTracker ({ provider, blockTracker }) {
// update or intialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider)
} else {
this._providerProxy = createSwappableProxy(provider)
} }
// override block tracler if (this._blockTrackerProxy) {
provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) this._blockTrackerProxy.setTarget(blockTracker)
// set as new provider } else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker)
}
// set new provider and blockTracker
this._provider = provider this._provider = provider
this._proxy.setTarget(provider) this._blockTracker = blockTracker
} }
_logBlock (block) { _logBlock (block) {

View File

@ -1,14 +1,14 @@
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const extend = require('xtend') const extend = require('xtend')
const BN = require('ethereumjs-util').BN
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const log = require('loglevel') const log = require('loglevel')
const pify = require('pify')
class RecentBlocksController { class RecentBlocksController {
/** /**
* Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled * Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled
* upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event * upon the controller's construction and then the list is updated when the given block tracker gets a 'latest' event
* (indicating that there is a new block to process). * (indicating that there is a new block to process).
* *
* @typedef {Object} RecentBlocksController * @typedef {Object} RecentBlocksController
@ -16,7 +16,7 @@ class RecentBlocksController {
* @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain * @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain
* @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance. * @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance.
* @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction, * @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction,
* listens for 'block' events so that new blocks can be processed and added to storage. * listens for 'latest' events so that new blocks can be processed and added to storage.
* @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider * @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider
* @property {number} historyLength The maximum length of blocks to track * @property {number} historyLength The maximum length of blocks to track
* @property {object} store Stores the recentBlocks * @property {object} store Stores the recentBlocks
@ -34,7 +34,13 @@ class RecentBlocksController {
}, opts.initState) }, opts.initState)
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this.blockTracker.on('block', this.processBlock.bind(this)) this.blockTracker.on('latest', async (newBlockNumberHex) => {
try {
await this.processBlock(newBlockNumberHex)
} catch (err) {
log.error(err)
}
})
this.backfill() this.backfill()
} }
@ -55,7 +61,11 @@ class RecentBlocksController {
* @param {object} newBlock The new block to modify and add to the recentBlocks array * @param {object} newBlock The new block to modify and add to the recentBlocks array
* *
*/ */
processBlock (newBlock) { async processBlock (newBlockNumberHex) {
const newBlockNumber = Number.parseInt(newBlockNumberHex, 16)
const newBlock = await this.getBlockByNumber(newBlockNumber, true)
if (!newBlock) return
const block = this.mapTransactionsToPrices(newBlock) const block = this.mapTransactionsToPrices(newBlock)
const state = this.store.getState() const state = this.store.getState()
@ -108,9 +118,9 @@ class RecentBlocksController {
} }
/** /**
* On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks * On this.blockTracker's first 'latest' event after this RecentBlocksController's instantiation, the store.recentBlocks
* array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first * array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first
* 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying * 'latest' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying
* the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest. * the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest.
* *
* Each iteration over the block numbers is delayed by 100 milliseconds. * Each iteration over the block numbers is delayed by 100 milliseconds.
@ -118,18 +128,17 @@ class RecentBlocksController {
* @returns {Promise<void>} Promises undefined * @returns {Promise<void>} Promises undefined
*/ */
async backfill () { async backfill () {
this.blockTracker.once('block', async (block) => { this.blockTracker.once('latest', async (blockNumberHex) => {
const currentBlockNumber = Number.parseInt(block.number, 16) const currentBlockNumber = Number.parseInt(blockNumberHex, 16)
const blocksToFetch = Math.min(currentBlockNumber, this.historyLength) const blocksToFetch = Math.min(currentBlockNumber, this.historyLength)
const prevBlockNumber = currentBlockNumber - 1 const prevBlockNumber = currentBlockNumber - 1
const targetBlockNumbers = Array(blocksToFetch).fill().map((_, index) => prevBlockNumber - index) const targetBlockNumbers = Array(blocksToFetch).fill().map((_, index) => prevBlockNumber - index)
await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => { await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => {
try { try {
const newBlock = await this.getBlockByNumber(targetBlockNumber) const newBlock = await this.getBlockByNumber(targetBlockNumber, true)
if (!newBlock) return
if (newBlock) { this.backfillBlock(newBlock)
this.backfillBlock(newBlock)
}
} catch (e) { } catch (e) {
log.error(e) log.error(e)
} }
@ -137,18 +146,6 @@ class RecentBlocksController {
}) })
} }
/**
* A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await
*
* @returns {Promise<void>} Promises undefined
*
*/
async wait () {
return new Promise((resolve) => {
setTimeout(resolve, 100)
})
}
/** /**
* Uses EthQuery to get a block that has a given block number. * Uses EthQuery to get a block that has a given block number.
* *
@ -157,13 +154,8 @@ class RecentBlocksController {
* *
*/ */
async getBlockByNumber (number) { async getBlockByNumber (number) {
const bn = new BN(number) const blockNumberHex = '0x' + number.toString(16)
return new Promise((resolve, reject) => { return await pify(this.ethQuery.getBlockByNumber).call(this.ethQuery, blockNumberHex, true)
this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => {
if (err) reject(err)
resolve(block)
})
})
} }
} }

View File

@ -65,6 +65,7 @@ class TransactionController extends EventEmitter {
this.store = this.txStateManager.store this.store = this.txStateManager.store
this.nonceTracker = new NonceTracker({ this.nonceTracker = new NonceTracker({
provider: this.provider, provider: this.provider,
blockTracker: this.blockTracker,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
}) })
@ -78,13 +79,17 @@ class TransactionController extends EventEmitter {
}) })
this.txStateManager.store.subscribe(() => this.emit('update:badge')) this.txStateManager.store.subscribe(() => this.emit('update:badge'))
this._setupListners() this._setupListeners()
// memstore is computed from a few different stores // memstore is computed from a few different stores
this._updateMemstore() this._updateMemstore()
this.txStateManager.store.subscribe(() => this._updateMemstore()) this.txStateManager.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore())
// request state update to finalize initialization
this._updatePendingTxsAfterFirstBlock()
} }
/** @returns {number} the chainId*/ /** @returns {number} the chainId*/
getChainId () { getChainId () {
const networkState = this.networkStore.getState() const networkState = this.networkStore.getState()
@ -311,6 +316,11 @@ class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusSubmitted(txId) this.txStateManager.setTxStatusSubmitted(txId)
} }
confirmTransaction (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
this._markNonceDuplicatesDropped(txId)
}
/** /**
Convenience method for the ui thats sets the transaction to rejected Convenience method for the ui thats sets the transaction to rejected
@param txId {number} - the tx's Id @param txId {number} - the tx's Id
@ -354,6 +364,14 @@ class TransactionController extends EventEmitter {
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
} }
// called once on startup
async _updatePendingTxsAfterFirstBlock () {
// wait for first block so we know we're ready
await this.blockTracker.getLatestBlock()
// get status update for all pending transactions (for the current network)
await this.pendingTxTracker.updatePendingTxs()
}
/** /**
If transaction controller was rebooted with transactions that are uncompleted If transaction controller was rebooted with transactions that are uncompleted
in steps of the transaction signing or user confirmation process it will either in steps of the transaction signing or user confirmation process it will either
@ -386,14 +404,14 @@ class TransactionController extends EventEmitter {
is called in constructor applies the listeners for pendingTxTracker txStateManager is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker and blockTracker
*/ */
_setupListners () { _setupListeners () {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this._setupBlockTrackerListener()
this.pendingTxTracker.on('tx:warning', (txMeta) => { this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
}) })
this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId))
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) { if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber txMeta.firstRetryBlockNumber = latestBlockNumber
@ -405,13 +423,6 @@ class TransactionController extends EventEmitter {
txMeta.retryCount++ txMeta.retryCount++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
}) })
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
} }
/** /**
@ -435,6 +446,40 @@ class TransactionController extends EventEmitter {
}) })
} }
_setupBlockTrackerListener () {
let listenersAreActive = false
const latestBlockHandler = this._onLatestBlock.bind(this)
const blockTracker = this.blockTracker
const txStateManager = this.txStateManager
txStateManager.on('tx:status-update', updateSubscription)
updateSubscription()
function updateSubscription () {
const pendingTxs = txStateManager.getPendingTransactions()
if (!listenersAreActive && pendingTxs.length > 0) {
blockTracker.on('latest', latestBlockHandler)
listenersAreActive = true
} else if (listenersAreActive && !pendingTxs.length) {
blockTracker.removeListener('latest', latestBlockHandler)
listenersAreActive = false
}
}
}
async _onLatestBlock (blockNumber) {
try {
await this.pendingTxTracker.updatePendingTxs()
} catch (err) {
log.error(err)
}
try {
await this.pendingTxTracker.resubmitPendingTxs(blockNumber)
} catch (err) {
log.error(err)
}
}
/** /**
Updates the memStore in transaction controller Updates the memStore in transaction controller
*/ */

View File

@ -12,8 +12,9 @@ const Mutex = require('await-semaphore').Mutex
*/ */
class NonceTracker { class NonceTracker {
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider this.provider = provider
this.blockTracker = blockTracker
this.ethQuery = new EthQuery(provider) this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions this.getConfirmedTransactions = getConfirmedTransactions
@ -34,7 +35,7 @@ class NonceTracker {
* @typedef NonceDetails * @typedef NonceDetails
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction. * @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method. * @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
* @property {number} highetSuggested - The maximum between the other two, the number returned. * @property {number} highestSuggested - The maximum between the other two, the number returned.
*/ */
/** /**
@ -80,15 +81,6 @@ class NonceTracker {
} }
} }
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 () { async _globalMutexFree () {
const globalMutex = this._lookupMutex('global') const globalMutex = this._lookupMutex('global')
const releaseLock = await globalMutex.acquire() const releaseLock = await globalMutex.acquire()
@ -114,9 +106,8 @@ class NonceTracker {
// calculate next nonce // calculate next nonce
// we need to make sure our base count // we need to make sure our base count
// and pending count are from the same block // and pending count are from the same block
const currentBlock = await this._getCurrentBlock() const blockNumber = await this.blockTracker.getLatestBlock()
const blockNumber = currentBlock.blockNumber const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber)
const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
const baseCount = baseCountBN.toNumber() const baseCount = baseCountBN.toNumber()
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
const nonceDetails = { blockNumber, baseCount } const nonceDetails = { blockNumber, baseCount }
@ -165,15 +156,6 @@ class NonceTracker {
return { name: 'local', nonce: highest, details: { startPoint, 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
/**
@returns {Object} the current blockTracker
*/
_getBlockTracker () {
return this.provider._blockTracker
}
} }
module.exports = NonceTracker module.exports = NonceTracker

View File

@ -1,6 +1,7 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const log = require('loglevel') const log = require('loglevel')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
/** /**
Event emitter utility class for tracking the transactions as they<br> Event emitter utility class for tracking the transactions as they<br>
@ -23,55 +24,26 @@ class PendingTransactionTracker extends EventEmitter {
super() super()
this.query = new EthQuery(config.provider) this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker this.nonceTracker = config.nonceTracker
// default is one day
this.getPendingTransactions = config.getPendingTransactions this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions this.getCompletedTransactions = config.getCompletedTransactions
this.publishTransaction = config.publishTransaction this.publishTransaction = config.publishTransaction
this._checkPendingTxs() this.confirmTransaction = config.confirmTransaction
} }
/** /**
checks if a signed tx is in a block and checks the network for signed txs and releases the nonce global lock if it is
if it is included emits tx status as 'confirmed'
@param block {object}, a full block
@emits tx:confirmed
@emits tx:failed
*/ */
checkForTxInBlock (block) { async updatePendingTxs () {
const signedTxList = this.getPendingTransactions() // in order to keep the nonceTracker accurate we block it while updating pending transactions
if (!signedTxList.length) return const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
signedTxList.forEach((txMeta) => { try {
const txHash = txMeta.hash const pendingTxs = this.getPendingTransactions()
const txId = txMeta.id await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)))
} catch (err) {
if (!txHash) { log.error('PendingTransactionTracker - Error updating pending transactions')
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') log.error(err)
noTxHashErr.name = 'NoTxHashError'
this.emit('tx:failed', txId, noTxHashErr)
return
}
block.transactions.forEach((tx) => {
if (tx.hash === txHash) this.emit('tx:confirmed', txId)
})
})
}
/**
asks the network for the transaction to see if a block number is included on it
if we have skipped/missed blocks
@param object - oldBlock newBlock
*/
queryPendingTxs ({ oldBlock, newBlock }) {
// check pending transactions on start
if (!oldBlock) {
this._checkPendingTxs()
return
} }
// if we synced by more than one block, check for missed pending transactions nonceGlobalLock.releaseLock()
const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16)
if (diff > 1) this._checkPendingTxs()
} }
/** /**
@ -79,11 +51,11 @@ class PendingTransactionTracker extends EventEmitter {
@param block {object} - a block object @param block {object} - a block object
@emits tx:warning @emits tx:warning
*/ */
resubmitPendingTxs (block) { resubmitPendingTxs (blockNumber) {
const pending = this.getPendingTransactions() const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit // only try resubmitting if their are transactions to resubmit
if (!pending.length) return if (!pending.length) return
pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => { pending.forEach((txMeta) => this._resubmitTx(txMeta, blockNumber).catch((err) => {
/* /*
Dont marked as failed if the error is a "known" transaction warning Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce "there is already a transaction with the same sender-nonce
@ -145,6 +117,7 @@ class PendingTransactionTracker extends EventEmitter {
this.emit('tx:retry', txMeta) this.emit('tx:retry', txMeta)
return txHash return txHash
} }
/** /**
Ask the network for the transaction to see if it has been include in a block Ask the network for the transaction to see if it has been include in a block
@param txMeta {Object} - the txMeta object @param txMeta {Object} - the txMeta object
@ -174,9 +147,8 @@ class PendingTransactionTracker extends EventEmitter {
} }
// get latest transaction status // get latest transaction status
let txParams
try { try {
txParams = await this.query.getTransactionByHash(txHash) const txParams = await this.query.getTransactionByHash(txHash)
if (!txParams) return if (!txParams) return
if (txParams.blockNumber) { if (txParams.blockNumber) {
this.emit('tx:confirmed', txId) this.emit('tx:confirmed', txId)
@ -190,27 +162,13 @@ class PendingTransactionTracker extends EventEmitter {
} }
} }
/**
checks the network for signed txs and releases the nonce global lock if it is
*/
async _checkPendingTxs () {
const signedTxList = this.getPendingTransactions()
// in order to keep the nonceTracker accurate we block it while updating pending transactions
const { releaseLock } = await this.nonceTracker.getGlobalLock()
try {
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
} catch (err) {
log.error('PendingTransactionWatcher - Error updating pending transactions')
log.error(err)
}
releaseLock()
}
/** /**
checks to see if a confirmed txMeta has the same nonce checks to see if a confirmed txMeta has the same nonce
@param txMeta {Object} - txMeta object @param txMeta {Object} - txMeta object
@returns {boolean} @returns {boolean}
*/ */
async _checkIfNonceIsTaken (txMeta) { async _checkIfNonceIsTaken (txMeta) {
const address = txMeta.txParams.from const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address) const completed = this.getCompletedTransactions(address)

View File

@ -25,7 +25,7 @@ class TxGasUtil {
@returns {object} the txMeta object with the gas written to the txParams @returns {object} the txMeta object with the gas written to the txParams
*/ */
async analyzeGasUsage (txMeta) { async analyzeGasUsage (txMeta) {
const block = await this.query.getBlockByNumber('latest', true) const block = await this.query.getBlockByNumber('latest', false)
let estimatedGasHex let estimatedGasHex
try { try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)

View File

@ -7,14 +7,13 @@
* on each new block. * on each new block.
*/ */
const async = require('async')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const EventEmitter = require('events').EventEmitter const log = require('loglevel')
function noop () {} const pify = require('pify')
class AccountTracker extends EventEmitter { class AccountTracker {
/** /**
* This module is responsible for tracking any number of accounts and caching their current balances & transaction * This module is responsible for tracking any number of accounts and caching their current balances & transaction
@ -35,8 +34,6 @@ class AccountTracker extends EventEmitter {
* *
*/ */
constructor (opts = {}) { constructor (opts = {}) {
super()
const initState = { const initState = {
accounts: {}, accounts: {},
currentBlockGasLimit: '', currentBlockGasLimit: '',
@ -44,12 +41,12 @@ class AccountTracker extends EventEmitter {
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
this._provider = opts.provider this._provider = opts.provider
this._query = new EthQuery(this._provider) this._query = pify(new EthQuery(this._provider))
this._blockTracker = opts.blockTracker this._blockTracker = opts.blockTracker
// subscribe to latest block // subscribe to latest block
this._blockTracker.on('block', this._updateForBlock.bind(this)) this._blockTracker.on('latest', this._updateForBlock.bind(this))
// blockTracker.currentBlock may be null // blockTracker.currentBlock may be null
this._currentBlockNumber = this._blockTracker.currentBlock this._currentBlockNumber = this._blockTracker.getCurrentBlock()
} }
/** /**
@ -67,49 +64,57 @@ class AccountTracker extends EventEmitter {
const accounts = this.store.getState().accounts const accounts = this.store.getState().accounts
const locals = Object.keys(accounts) const locals = Object.keys(accounts)
const toAdd = [] const accountsToAdd = []
addresses.forEach((upstream) => { addresses.forEach((upstream) => {
if (!locals.includes(upstream)) { if (!locals.includes(upstream)) {
toAdd.push(upstream) accountsToAdd.push(upstream)
} }
}) })
const toRemove = [] const accountsToRemove = []
locals.forEach((local) => { locals.forEach((local) => {
if (!addresses.includes(local)) { if (!addresses.includes(local)) {
toRemove.push(local) accountsToRemove.push(local)
} }
}) })
toAdd.forEach(upstream => this.addAccount(upstream)) this.addAccounts(accountsToAdd)
toRemove.forEach(local => this.removeAccount(local)) this.removeAccount(accountsToRemove)
this._updateAccounts()
} }
/** /**
* Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be * Adds new addresses to track the balances of
* given a balance as long this._currentBlockNumber is defined. * 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 * @param {array} addresses An array of hex addresses of new accounts to track
* *
*/ */
addAccount (address) { addAccounts (addresses) {
const accounts = this.store.getState().accounts const accounts = this.store.getState().accounts
accounts[address] = {} // add initial state for addresses
addresses.forEach(address => {
accounts[address] = {}
})
// save accounts state
this.store.updateState({ accounts }) this.store.updateState({ accounts })
// fetch balances for the accounts if there is block number ready
if (!this._currentBlockNumber) return if (!this._currentBlockNumber) return
this._updateAccount(address) addresses.forEach(address => this._updateAccount(address))
} }
/** /**
* Removes an account from this AccountTracker's accounts object * Removes accounts from being tracked
* *
* @param {string} address A hex address of a the account to remove * @param {array} an array of hex addresses to stop tracking
* *
*/ */
removeAccount (address) { removeAccount (addresses) {
const accounts = this.store.getState().accounts const accounts = this.store.getState().accounts
delete accounts[address] // remove each state object
addresses.forEach(address => {
delete accounts[address]
})
// save accounts state
this.store.updateState({ accounts }) this.store.updateState({ accounts })
} }
@ -118,71 +123,56 @@ class AccountTracker extends EventEmitter {
* via EthQuery * via EthQuery
* *
* @private * @private
* @param {object} block Data about the block that contains the data to update to. * @param {number} blockNumber the block number to update to.
* @fires 'block' The updated state, if all account updates are successful * @fires 'block' The updated state, if all account updates are successful
* *
*/ */
_updateForBlock (block) { async _updateForBlock (blockNumber) {
this._currentBlockNumber = block.number this._currentBlockNumber = blockNumber
const currentBlockGasLimit = block.gasLimit
// block gasLimit polling shouldn't be in account-tracker shouldn't be here...
const currentBlock = await this._query.getBlockByNumber(blockNumber, false)
if (!currentBlock) return
const currentBlockGasLimit = currentBlock.gasLimit
this.store.updateState({ currentBlockGasLimit }) this.store.updateState({ currentBlockGasLimit })
async.parallel([ try {
this._updateAccounts.bind(this), await this._updateAccounts()
], (err) => { } catch (err) {
if (err) return console.error(err) log.error(err)
this.emit('block', this.store.getState()) }
})
} }
/** /**
* Calls this._updateAccount for each account in this.store * 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 * @returns {Promise} after all account balances updated
* *
*/ */
_updateAccounts (cb = noop) { async _updateAccounts () {
const accounts = this.store.getState().accounts const accounts = this.store.getState().accounts
const addresses = Object.keys(accounts) const addresses = Object.keys(accounts)
async.each(addresses, this._updateAccount.bind(this), cb) await Promise.all(addresses.map(this._updateAccount.bind(this)))
} }
/** /**
* Updates the current balance of an account. Gets an updated balance via this._getAccount. * Updates the current balance of an account.
* *
* @private * @private
* @param {string} address A hex address of a the account to be updated * @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 * @returns {Promise} after the account balance is updated
* *
*/ */
_updateAccount (address, cb = noop) { async _updateAccount (address) {
this._getAccount(address, (err, result) => { // query balance
if (err) return cb(err) const balance = await this._query.getBalance(address)
result.address = address const result = { address, balance }
const accounts = this.store.getState().accounts // update accounts state
// only populate if the entry is still present const { accounts } = this.store.getState()
if (accounts[address]) { // only populate if the entry is still present
accounts[address] = result if (!accounts[address]) return
this.store.updateState({ accounts }) accounts[address] = result
} this.store.updateState({ accounts })
cb(null, result)
})
}
/**
* 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({
balance: query.getBalance.bind(query, address),
}, cb)
} }
} }

View File

@ -1,42 +0,0 @@
/**
* 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 = /** @type {any} */ (new Proxy({}, {
get: (_, name) => {
// intercept listeners
if (name === 'on') return addListener
if (name === 'setTarget') return setTarget
if (name === 'proxyEventHandlers') return eventHandlers
return (/** @type {any} */ (target))[name]
},
set: (_, name, value) => {
target[name] = value
return true
},
}))
function setTarget (/** @type {EventEmitter} */ eventEmitter) {
target = eventEmitter
// migrate listeners
Object.keys(eventHandlers).forEach((name) => {
/** @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)
target.on(name, handler)
}
if (listeners) proxy.setTarget(eventEmitter)
return proxy
}

View File

@ -69,10 +69,39 @@ module.exports = class MessageManager extends EventEmitter {
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. * 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. * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {promise} after signature has been
*
*/
addUnapprovedMessageAsync (msgParams, req) {
return new Promise((resolve, reject) => {
const msgId = this.addUnapprovedMessage(msgParams, req)
// await finished
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
})
}
/**
* 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.
* @param {Object} req (optional) The original request object where the origin may be specificied
* @returns {number} The id of the newly created message. * @returns {number} The id of the newly created message.
* *
*/ */
addUnapprovedMessage (msgParams) { addUnapprovedMessage (msgParams, req) {
// add origin from request
if (req) msgParams.origin = req.origin
msgParams.data = normalizeMsgData(msgParams.data) msgParams.data = normalizeMsgData(msgParams.data)
// create txData obj with parameters and meta data // create txData obj with parameters and meta data
var time = (new Date()).getTime() var time = (new Date()).getTime()

View File

@ -73,11 +73,43 @@ module.exports = class PersonalMessageManager extends EventEmitter {
* this.memStore. * this.memStore.
* *
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {promise} When the message has been signed or rejected
*
*/
addUnapprovedMessageAsync (msgParams, req) {
return new Promise((resolve, reject) => {
if (!msgParams.from) {
reject(new Error('MetaMask Message Signature: from field is required.'))
}
const msgId = this.addUnapprovedMessage(msgParams, req)
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
})
}
/**
* 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.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {number} The id of the newly created PersonalMessage. * @returns {number} The id of the newly created PersonalMessage.
* *
*/ */
addUnapprovedMessage (msgParams) { addUnapprovedMessage (msgParams, req) {
log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
// add origin from request
if (req) msgParams.origin = req.origin
msgParams.data = this.normalizeMsgData(msgParams.data) msgParams.data = this.normalizeMsgData(msgParams.data)
// create txData obj with parameters and meta data // create txData obj with parameters and meta data
var time = (new Date()).getTime() var time = (new Date()).getTime()
@ -257,4 +289,3 @@ module.exports = class PersonalMessageManager extends EventEmitter {
} }
} }

View File

@ -70,11 +70,11 @@ function simplifyErrorMessages (report) {
function rewriteErrorMessages (report, rewriteFn) { function rewriteErrorMessages (report, rewriteFn) {
// rewrite top level message // rewrite top level message
if (report.message) report.message = rewriteFn(report.message) if (typeof report.message === 'string') report.message = rewriteFn(report.message)
// rewrite each exception message // rewrite each exception message
if (report.exception && report.exception.values) { if (report.exception && report.exception.values) {
report.exception.values.forEach(item => { report.exception.values.forEach(item => {
item.value = rewriteFn(item.value) if (typeof item.value === 'string') item.value = rewriteFn(item.value)
}) })
} }
} }

View File

@ -72,11 +72,40 @@ module.exports = class TypedMessageManager extends EventEmitter {
* this.memStore. Before any of this is done, msgParams are validated * 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. * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {promise} When the message has been signed or rejected
*
*/
addUnapprovedMessageAsync (msgParams, req) {
return new Promise((resolve, reject) => {
const msgId = this.addUnapprovedMessage(msgParams, req)
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return resolve(data.rawSig)
case 'rejected':
return reject(new Error('MetaMask Message Signature: User denied message signature.'))
default:
return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
})
}
/**
* 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.
* @param {Object} req (optional) The original request object possibly containing the origin
* @returns {number} The id of the newly created TypedMessage. * @returns {number} The id of the newly created TypedMessage.
* *
*/ */
addUnapprovedMessage (msgParams) { addUnapprovedMessage (msgParams, req) {
this.validateParams(msgParams) this.validateParams(msgParams)
// add origin from request
if (req) msgParams.origin = req.origin
log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
// create txData obj with parameters and meta data // create txData obj with parameters and meta data

View File

@ -127,7 +127,21 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) {
return targetBN.mul(numBN).div(denomBN) return targetBN.mul(numBN).div(denomBN)
} }
function applyListeners (listeners, emitter) {
Object.keys(listeners).forEach((key) => {
emitter.on(key, listeners[key])
})
}
function removeListeners (listeners, emitter) {
Object.keys(listeners).forEach((key) => {
emitter.removeListener(key, listeners[key])
})
}
module.exports = { module.exports = {
removeListeners,
applyListeners,
getPlatform, getPlatform,
getStack, getStack,
getEnvironmentType, getEnvironmentType,

View File

@ -46,7 +46,6 @@ const BN = require('ethereumjs-util').BN
const GWEI_BN = new BN('1000000000') const GWEI_BN = new BN('1000000000')
const percentile = require('percentile') const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack')
const log = require('loglevel') const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring') const TrezorKeyring = require('eth-trezor-keyring')
@ -107,8 +106,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.blacklistController.scheduleUpdates() this.blacklistController.scheduleUpdates()
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.initializeProvider()
this.blockTracker = this.provider._blockTracker this.provider = this.networkController.getProviderAndBlockTracker().provider
this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker
// token exchange rate tracker // token exchange rate tracker
this.tokenRatesController = new TokenRatesController({ this.tokenRatesController = new TokenRatesController({
@ -252,28 +252,22 @@ module.exports = class MetamaskController extends EventEmitter {
static: { static: {
eth_syncing: false, eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`, web3_clientVersion: `MetaMask/v${version}`,
eth_sendTransaction: (payload, next, end) => {
const origin = payload.origin
const txParams = payload.params[0]
nodeify(this.txController.newUnapprovedTransaction, this.txController)(txParams, { origin }, end)
},
}, },
// account mgmt // account mgmt
getAccounts: (cb) => { getAccounts: async () => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const result = []
const selectedAddress = this.preferencesController.getSelectedAddress() const selectedAddress = this.preferencesController.getSelectedAddress()
// only show address if account is unlocked // only show address if account is unlocked
if (isUnlocked && selectedAddress) { if (isUnlocked && selectedAddress) {
result.push(selectedAddress) return [selectedAddress]
} else {
return []
} }
cb(null, result)
}, },
// tx signing // tx signing
// old style msg signing processTransaction: this.newUnapprovedTransaction.bind(this),
processMessage: this.newUnsignedMessage.bind(this), // msg signing
// personal_sign msg signing processEthSignMessage: this.newUnsignedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
processTypedMessage: this.newUnsignedTypedMessage.bind(this), processTypedMessage: this.newUnsignedTypedMessage.bind(this),
} }
@ -809,6 +803,18 @@ module.exports = class MetamaskController extends EventEmitter {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Identity Management (signature operations) // Identity Management (signature operations)
/**
* Called when a Dapp suggests a new tx to be signed.
* this wrapper needs to exist so we can provide a reference to
* "newUnapprovedTransaction" before "txController" is instantiated
*
* @param {Object} msgParams - The params passed to eth_sign.
* @param {Object} req - (optional) the original request, containing the origin
*/
async newUnapprovedTransaction (txParams, req) {
return await this.txController.newUnapprovedTransaction(txParams, req)
}
// eth_sign methods: // eth_sign methods:
/** /**
@ -820,20 +826,11 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_sign. * @param {Object} msgParams - The params passed to eth_sign.
* @param {Function} cb = The callback function called with the signature. * @param {Function} cb = The callback function called with the signature.
*/ */
newUnsignedMessage (msgParams, cb) { newUnsignedMessage (msgParams, req) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams) const promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req)
this.sendUpdate() this.sendUpdate()
this.opts.showUnconfirmedMessage() this.opts.showUnconfirmedMessage()
this.messageManager.once(`${msgId}:finished`, (data) => { return promise
switch (data.status) {
case 'signed':
return cb(null, data.rawSig)
case 'rejected':
return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
default:
return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
}
})
} }
/** /**
@ -887,24 +884,11 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - The callback function called with the signature. * @param {Function} cb - The callback function called with the signature.
* Passed back to the requesting Dapp. * Passed back to the requesting Dapp.
*/ */
newUnsignedPersonalMessage (msgParams, cb) { async newUnsignedPersonalMessage (msgParams, req) {
if (!msgParams.from) { const promise = this.personalMessageManager.addUnapprovedMessageAsync(msgParams, req)
return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.')))
}
const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
this.sendUpdate() this.sendUpdate()
this.opts.showUnconfirmedMessage() this.opts.showUnconfirmedMessage()
this.personalMessageManager.once(`${msgId}:finished`, (data) => { return promise
switch (data.status) {
case 'signed':
return cb(null, data.rawSig)
case 'rejected':
return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
default:
return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
}
})
} }
/** /**
@ -953,26 +937,11 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Object} msgParams - The params passed to eth_signTypedData.
* @param {Function} cb - The callback function, called with the signature. * @param {Function} cb - The callback function, called with the signature.
*/ */
newUnsignedTypedMessage (msgParams, cb) { newUnsignedTypedMessage (msgParams, req) {
let msgId const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req)
try { this.sendUpdate()
msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) this.opts.showUnconfirmedMessage()
this.sendUpdate() return promise
this.opts.showUnconfirmedMessage()
} catch (e) {
return cb(e)
}
this.typedMessageManager.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return cb(null, data.rawSig)
case 'rejected':
return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
default:
return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
}
})
} }
/** /**
@ -1237,7 +1206,7 @@ module.exports = class MetamaskController extends EventEmitter {
// create filter polyfill middleware // create filter polyfill middleware
const filterMiddleware = createFilterMiddleware({ const filterMiddleware = createFilterMiddleware({
provider: this.provider, provider: this.provider,
blockTracker: this.provider._blockTracker, blockTracker: this.blockTracker,
}) })
engine.push(createOriginMiddleware({ origin })) engine.push(createOriginMiddleware({ origin }))

View File

@ -1,10 +1,9 @@
### Developing on Dependencies ### Developing on Dependencies
To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies: To enjoy the live-reloading that `gulp dev` offers while working on the dependencies:
1. Clone the dependency locally. 1. Clone the dependency locally.
2. `npm install` in its folder. 2. `npm install` in its folder.
3. Run `npm link` in its folder. 3. Run `npm link` in its folder.
4. Run `npm link $DEP_NAME` in this project folder. 4. Run `npm link $DEP_NAME` in this project folder.
5. Next time you `npm start` it will watch the dependency for changes as well! 5. Next time you `npm start` it will watch the dependency for changes as well!

View File

@ -89,8 +89,6 @@ MetaMask has two kinds of [duplex stream APIs](https://github.com/substack/strea
If you are making a MetaMask-powered browser for a new platform, one of the trickiest tasks will be injecting the Web3 API into websites that are visited. On WebExtensions, we actually have to pipe data through a total of three JS contexts just to let sites talk to our background process (site -> contentscript -> background). If you are making a MetaMask-powered browser for a new platform, one of the trickiest tasks will be injecting the Web3 API into websites that are visited. On WebExtensions, we actually have to pipe data through a total of three JS contexts just to let sites talk to our background process (site -> contentscript -> background).
To make this as easy as possible, we use one of our favorite internal tools, [web3-provider-engine](https://www.npmjs.com/package/web3-provider-engine) to construct a custom web3 provider object whose source of truth is a stream that we connect to remotely.
To see how we do that, you can refer to the [inpage script](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/inpage.js) that we inject into every website. There you can see it creates a multiplex stream to the background, and uses it to initialize what we call the [inpage-provider](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/lib/inpage-provider.js), which you can see stubs a few methods out, but mostly just passes calls to `sendAsync` through the stream it's passed! That's really all the magic that's needed to create a web3-like API in a remote context, once you have a stream to MetaMask available. To see how we do that, you can refer to the [inpage script](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/inpage.js) that we inject into every website. There you can see it creates a multiplex stream to the background, and uses it to initialize what we call the [inpage-provider](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/lib/inpage-provider.js), which you can see stubs a few methods out, but mostly just passes calls to `sendAsync` through the stream it's passed! That's really all the magic that's needed to create a web3-like API in a remote context, once you have a stream to MetaMask available.
In `inpage.js` you can see we create a `PortStream`, that's just a class we use to wrap WebExtension ports as streams, so we can reuse our favorite stream abstraction over the more irregular API surface of the WebExtension. In a new platform, you will probably need to construct this stream differently. The key is that you need to construct a stream that talks from the site context to the background. Once you have that set up, it works like magic! In `inpage.js` you can see we create a `PortStream`, that's just a class we use to wrap WebExtension ports as streams, so we can reuse our favorite stream abstraction over the more irregular API surface of the WebExtension. In a new platform, you will probably need to construct this stream differently. The key is that you need to construct a stream that talks from the site context to the background. Once you have that set up, it works like magic!

View File

@ -116,12 +116,25 @@ Notice.prototype.render = function () {
) )
} }
Notice.prototype.setInitialDisclaimerState = function () {
if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
this.setState({disclaimerDisabled: false})
}
}
Notice.prototype.componentDidMount = function () { Notice.prototype.componentDidMount = function () {
// eslint-disable-next-line react/no-find-dom-node // eslint-disable-next-line react/no-find-dom-node
var node = findDOMNode(this) var node = findDOMNode(this)
linker.setupListener(node) linker.setupListener(node)
if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { this.setInitialDisclaimerState()
this.setState({disclaimerDisabled: false}) }
Notice.prototype.componentDidUpdate = function (prevProps) {
const { notice: { id } = {} } = this.props
const { notice: { id: prevNoticeId } = {} } = prevProps
if (id !== prevNoticeId) {
this.setInitialDisclaimerState()
} }
} }

2817
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,7 @@
"test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales", "test:mascara:build:locales": "mkdirp dist/chrome && cp -R app/_locales dist/chrome/_locales",
"test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js", "test:mascara:build:background": "browserify mascara/src/background.js -o dist/mascara/background.js",
"test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js", "test:mascara:build:tests": "browserify test/integration/lib/first-time.js -o dist/mascara/tests.js",
"ganache:start": "ganache-cli -m 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'", "ganache:start": "ganache-cli --noVMErrorsOnRPCResponse -m 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'",
"sentry:publish": "node ./development/sentry-publish.js", "sentry:publish": "node ./development/sentry-publish.js",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
@ -105,10 +105,13 @@
"ensnare": "^1.0.0", "ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^4.0.1",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master", "eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
"eth-json-rpc-middleware": "^2.4.0",
"eth-keyring-controller": "^3.1.4",
"eth-ens-namehash": "^2.0.8", "eth-ens-namehash": "^2.0.8",
"eth-hd-keyring": "^1.2.2", "eth-hd-keyring": "^1.2.2",
"eth-json-rpc-filters": "^1.2.6", "eth-json-rpc-filters": "^2.1.1",
"eth-json-rpc-infura": "^3.0.0", "eth-json-rpc-infura": "^3.0.0",
"eth-method-registry": "^1.0.0", "eth-method-registry": "^1.0.0",
"eth-phishing-detect": "^1.1.4", "eth-phishing-detect": "^1.1.4",
@ -145,7 +148,7 @@
"iframe-stream": "^3.0.0", "iframe-stream": "^3.0.0",
"inject-css": "^0.1.1", "inject-css": "^0.1.1",
"jazzicon": "^1.2.0", "jazzicon": "^1.2.0",
"json-rpc-engine": "^3.6.1", "json-rpc-engine": "^3.7.3",
"json-rpc-middleware-stream": "^1.0.1", "json-rpc-middleware-stream": "^1.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2", "lodash.memoize": "^4.1.2",
@ -202,11 +205,11 @@
"shallow-copy": "0.0.1", "shallow-copy": "0.0.1",
"sw-controller": "^1.0.3", "sw-controller": "^1.0.3",
"sw-stream": "^2.0.2", "sw-stream": "^2.0.2",
"swappable-obj-proxy": "^1.0.2",
"textarea-caret": "^3.0.1", "textarea-caret": "^3.0.1",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "^0.20.1", "web3": "^0.20.1",
"web3-provider-engine": "^14.0.5",
"web3-stream-provider": "^3.0.1", "web3-stream-provider": "^3.0.1",
"webrtc-adapter": "^6.3.0", "webrtc-adapter": "^6.3.0",
"xtend": "^4.0.1" "xtend": "^4.0.1"
@ -249,6 +252,7 @@
"eth-json-rpc-middleware": "^1.6.0", "eth-json-rpc-middleware": "^1.6.0",
"eth-keyring-controller": "^3.3.1", "eth-keyring-controller": "^3.3.1",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"fs-extra": "^6.0.1",
"fs-promise": "^2.0.3", "fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0", "ganache-cli": "^6.1.0",
"ganache-core": "^2.1.5", "ganache-core": "^2.1.5",
@ -293,6 +297,7 @@
"open": "0.0.5", "open": "0.0.5",
"path": "^0.12.7", "path": "^0.12.7",
"png-file-stream": "^1.0.0", "png-file-stream": "^1.0.0",
"prepend-file": "^1.3.1",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"proxyquire": "2.0.1", "proxyquire": "2.0.1",
"qs": "^6.2.0", "qs": "^6.2.0",

View File

@ -2,8 +2,8 @@ const fs = require('fs')
const mkdirp = require('mkdirp') const mkdirp = require('mkdirp')
const pify = require('pify') const pify = require('pify')
const assert = require('assert') const assert = require('assert')
const {until} = require('selenium-webdriver')
const { delay } = require('../func') const { delay } = require('../func')
const { until } = require('selenium-webdriver')
module.exports = { module.exports = {
assertElementNotPresent, assertElementNotPresent,

View File

@ -525,6 +525,15 @@ describe('MetaMask', function () {
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('confirms a deploy contract transaction in the popup', async () => {
const windowHandles = await driver.getAllWindowHandles()
const popup = windowHandles[2]
await driver.switchTo().window(popup)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
})
it('calls and confirms a contract method where ETH is sent', async () => { it('calls and confirms a contract method where ETH is sent', async () => {
await driver.switchTo().window(dapp) await driver.switchTo().window(dapp)
await delay(regularDelayMs) await delay(regularDelayMs)
@ -626,20 +635,21 @@ describe('MetaMask', function () {
describe('Add a custom token from a dapp', () => { describe('Add a custom token from a dapp', () => {
it('creates a new token', async () => { it('creates a new token', async () => {
const windowHandles = await driver.getAllWindowHandles() let windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0] const extension = windowHandles[0]
const dapp = windowHandles[1] const dapp = windowHandles[1]
await delay(regularDelayMs * 2) await delay(regularDelayMs * 2)
await driver.switchTo().window(dapp) await driver.switchTo().window(dapp)
await delay(regularDelayMs) await delay(regularDelayMs * 2)
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`)) const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click() await createToken.click()
await delay(regularDelayMs) await delay(largeDelayMs)
await driver.switchTo().window(extension) windowHandles = await driver.getAllWindowHandles()
await loadExtension(driver, extensionId) const popup = windowHandles[2]
await driver.switchTo().window(popup)
await delay(regularDelayMs) await delay(regularDelayMs)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
@ -1014,4 +1024,4 @@ describe('MetaMask', function () {
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
}) })

View File

@ -1,14 +1,19 @@
require('chromedriver') require('chromedriver')
require('geckodriver') require('geckodriver')
const fs = require('fs') const fs = require('fs-extra')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
const pify = require('pify')
const prependFile = pify(require('prepend-file'))
const webdriver = require('selenium-webdriver') const webdriver = require('selenium-webdriver')
const Command = require('selenium-webdriver/lib/command').Command const Command = require('selenium-webdriver/lib/command').Command
const By = webdriver.By const By = webdriver.By
module.exports = { module.exports = {
delay, delay,
createModifiedTestBuild,
setupBrowserAndExtension,
verboseReportOnFailure,
buildChromeWebDriver, buildChromeWebDriver,
buildFirefoxWebdriver, buildFirefoxWebdriver,
installWebExt, installWebExt,
@ -20,6 +25,37 @@ function delay (time) {
return new Promise(resolve => setTimeout(resolve, time)) return new Promise(resolve => setTimeout(resolve, time))
} }
async function createModifiedTestBuild ({ browser, srcPath }) {
// copy build to test-builds directory
const extPath = path.resolve(`test-builds/${browser}`)
await fs.ensureDir(extPath)
await fs.copy(srcPath, extPath)
// inject METAMASK_TEST_CONFIG setting default test network
const config = { NetworkController: { provider: { type: 'localhost' } } }
await prependFile(`${extPath}/background.js`, `window.METAMASK_TEST_CONFIG=${JSON.stringify(config)};\n`)
return { extPath }
}
async function setupBrowserAndExtension ({ browser, extPath }) {
let driver, extensionId, extensionUri
if (browser === 'chrome') {
driver = buildChromeWebDriver(extPath)
extensionId = await getExtensionIdChrome(driver)
extensionUri = `chrome-extension://${extensionId}/home.html`
} else if (browser === 'firefox') {
driver = buildFirefoxWebdriver()
await installWebExt(driver, extPath)
await delay(700)
extensionId = await getExtensionIdFirefox(driver)
extensionUri = `moz-extension://${extensionId}/home.html`
} else {
throw new Error(`Unknown Browser "${browser}"`)
}
return { driver, extensionId, extensionUri }
}
function buildChromeWebDriver (extPath) { function buildChromeWebDriver (extPath) {
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile')) const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile'))
return new webdriver.Builder() return new webdriver.Builder()
@ -61,3 +97,13 @@ async function installWebExt (driver, extension) {
return await driver.schedule(cmd, 'installWebExt(' + extension + ')') return await driver.schedule(cmd, 'installWebExt(' + extension + ')')
} }
async function verboseReportOnFailure ({ browser, driver, title }) {
const artifactDir = `./test-artifacts/${browser}/${title}`
const filepathBase = `${artifactDir}/test-failure`
await fs.ensureDir(artifactDir)
const screenshot = await driver.takeScreenshot()
await fs.writeFile(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
const htmlSource = await driver.getPageSource()
await fs.writeFile(`${filepathBase}-dom.html`, htmlSource)
}

View File

@ -1,49 +1,41 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path') const path = require('path')
const assert = require('assert') const assert = require('assert')
const pify = require('pify') const { By, Key, until } = require('selenium-webdriver')
const webdriver = require('selenium-webdriver') const { delay, createModifiedTestBuild, setupBrowserAndExtension, verboseReportOnFailure } = require('./func')
const { By, Key, until } = webdriver
const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func')
describe('Metamask popup page', function () { describe('Metamask popup page', function () {
let driver, accountAddress, tokenAddress, extensionId const browser = process.env.SELENIUM_BROWSER
let driver, accountAddress, tokenAddress, extensionUri
this.timeout(0) this.timeout(0)
before(async function () { before(async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') { const srcPath = path.resolve(`dist/${browser}`)
const extPath = path.resolve('dist/chrome') const { extPath } = await createModifiedTestBuild({ browser, srcPath })
driver = buildChromeWebDriver(extPath) const installResult = await setupBrowserAndExtension({ browser, extPath })
extensionId = await getExtensionIdChrome(driver) driver = installResult.driver
await driver.get(`chrome-extension://${extensionId}/popup.html`) extensionUri = installResult.extensionUri
} else if (process.env.SELENIUM_BROWSER === 'firefox') { await driver.get(extensionUri)
const extPath = path.resolve('dist/firefox') await delay(300)
driver = buildFirefoxWebdriver()
await installWebExt(driver, extPath)
await delay(700)
extensionId = await getExtensionIdFirefox(driver)
await driver.get(`moz-extension://${extensionId}/popup.html`)
}
}) })
afterEach(async function () { afterEach(async function () {
// logs command not supported in firefox // logs command not supported in firefox
// https://github.com/SeleniumHQ/selenium/issues/2910 // https://github.com/SeleniumHQ/selenium/issues/2910
if (process.env.SELENIUM_BROWSER === 'chrome') { if (browser === 'chrome') {
// check for console errors // check for console errors
const errors = await checkBrowserForConsoleErrors() const errors = await checkBrowserForConsoleErrors()
if (errors.length) { if (errors.length) {
const errorReports = errors.map(err => err.message) const errorReports = errors.map(err => err.message)
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}` const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
this.test.error(new Error(errorMessage)) console.error(new Error(errorMessage))
} }
} }
// gather extra data if test failed // gather extra data if test failed
if (this.currentTest.state === 'failed') { if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest) await verboseReportOnFailure({ browser, driver, title: this.currentTest.title })
} }
}) })
@ -54,7 +46,6 @@ describe('Metamask popup page', function () {
describe('Setup', function () { describe('Setup', function () {
it('switches to Chrome extensions list', async function () { it('switches to Chrome extensions list', async function () {
await delay(300)
const windowHandles = await driver.getAllWindowHandles() const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[0]) await driver.switchTo().window(windowHandles[0])
}) })
@ -98,6 +89,7 @@ describe('Metamask popup page', function () {
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => { it('allows the button to be clicked when scrolled to the bottom of TOU', async () => {
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button')) const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button'))
await button.click() await button.click()
await delay(300)
}) })
it('shows privacy notice', async () => { it('shows privacy notice', async () => {
@ -108,7 +100,6 @@ describe('Metamask popup page', function () {
}) })
it('shows phishing notice', async () => { it('shows phishing notice', async () => {
await delay(300)
const noticeHeader = await driver.findElement(By.css('.terms-header')).getText() const noticeHeader = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(noticeHeader, 'PHISHING WARNING', 'shows phishing warning') assert.equal(noticeHeader, 'PHISHING WARNING', 'shows phishing warning')
const element = await driver.findElement(By.css('.markdown')) const element = await driver.findElement(By.css('.markdown'))
@ -295,11 +286,7 @@ describe('Metamask popup page', function () {
}) })
it('navigates back to MetaMask popup in the tab', async function () { it('navigates back to MetaMask popup in the tab', async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') { await driver.get(extensionUri)
await driver.get(`chrome-extension://${extensionId}/popup.html`)
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
await driver.get(`moz-extension://${extensionId}/popup.html`)
}
await delay(700) await delay(700)
}) })
}) })
@ -362,21 +349,4 @@ describe('Metamask popup page', function () {
return matchedErrorObjects return matchedErrorObjects
} }
async function verboseReportOnFailure (test) {
let artifactDir
if (process.env.SELENIUM_BROWSER === 'chrome') {
artifactDir = `./test-artifacts/chrome/${test.title}`
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
artifactDir = `./test-artifacts/firefox/${test.title}`
}
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
}) })

View File

@ -1,10 +1,21 @@
const Ganache = require('ganache-core')
const nock = require('nock')
import Enzyme from 'enzyme' import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-15' import Adapter from 'enzyme-adapter-react-15'
nock.disableNetConnect()
nock.enableNetConnect('localhost')
Enzyme.configure({ adapter: new Adapter() }) Enzyme.configure({ adapter: new Adapter() })
// disallow promises from swallowing errors // disallow promises from swallowing errors
enableFailureOnUnhandledPromiseRejection() enableFailureOnUnhandledPromiseRejection()
// ganache server
const server = Ganache.server()
server.listen(8545, () => {
console.log('Ganache Testrpc is running on "http://localhost:8545"')
})
// logging util // logging util
var log = require('loglevel') var log = require('loglevel')
log.setDefaultLevel(5) log.setDefaultLevel(5)
@ -14,6 +25,9 @@ global.log = log
// polyfills // polyfills
// //
// fetch
global.fetch = require('isomorphic-fetch')
// dom // dom
require('jsdom-global')() require('jsdom-global')()

16
test/lib/createTxMeta.js Normal file
View File

@ -0,0 +1,16 @@
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
module.exports = createTxMeta
function createTxMeta (partialMeta) {
const txMeta = Object.assign({
status: 'unapproved',
txParams: {},
}, partialMeta)
// initialize history
txMeta.history = []
// capture initial snapshot of txMeta for history
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history.push(snapshot)
return txMeta
}

View File

@ -1,6 +1,3 @@
// polyfill fetch
global.fetch = global.fetch || require('isomorphic-fetch')
const assert = require('assert') const assert = require('assert')
const nock = require('nock') const nock = require('nock')
const CurrencyController = require('../../../../app/scripts/controllers/currency') const CurrencyController = require('../../../../app/scripts/controllers/currency')

View File

@ -1,4 +1,5 @@
const assert = require('assert') const assert = require('assert')
const nock = require('nock')
const sinon = require('sinon') const sinon = require('sinon')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens') const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens')
@ -6,15 +7,34 @@ const NetworkController = require('../../../../app/scripts/controllers/network/n
const PreferencesController = require('../../../../app/scripts/controllers/preferences') const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('DetectTokensController', () => { describe('DetectTokensController', () => {
const sandbox = sinon.createSandbox() const sandbox = sinon.createSandbox()
let clock, keyringMemStore, network, preferences let clock, keyringMemStore, network, preferences, controller
beforeEach(async () => {
keyringMemStore = new ObservableStore({ isUnlocked: false}) const noop = () => {}
network = new NetworkController({ provider: { type: 'mainnet' }})
preferences = new PreferencesController({ network }) const networkControllerProviderConfig = {
}) getAccounts: noop,
after(() => { }
sandbox.restore()
beforeEach(async () => {
nock('https://api.infura.io')
.get(/.*/)
.reply(200)
keyringMemStore = new ObservableStore({ isUnlocked: false})
network = new NetworkController()
preferences = new PreferencesController({ network })
controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
network.initializeProvider(networkControllerProviderConfig)
})
after(() => {
sandbox.restore()
nock.cleanAll()
}) })
it('should poll on correct interval', async () => { it('should poll on correct interval', async () => {
@ -26,7 +46,10 @@ describe('DetectTokensController', () => {
it('should be called on every polling period', async () => { it('should be called on every polling period', async () => {
clock = sandbox.useFakeTimers() clock = sandbox.useFakeTimers()
const network = new NetworkController()
network.initializeProvider(networkControllerProviderConfig)
network.setProviderType('mainnet') network.setProviderType('mainnet')
const preferences = new PreferencesController({ network })
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -44,8 +67,6 @@ describe('DetectTokensController', () => {
}) })
it('should not check tokens while in test network', async () => { it('should not check tokens while in test network', async () => {
network.setProviderType('rinkeby')
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -58,7 +79,6 @@ describe('DetectTokensController', () => {
}) })
it('should only check and add tokens while in main network', async () => { it('should only check and add tokens while in main network', async () => {
network.setProviderType('mainnet')
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
@ -75,7 +95,6 @@ describe('DetectTokensController', () => {
}) })
it('should not detect same token while in main network', async () => { it('should not detect same token while in main network', async () => {
network.setProviderType('mainnet')
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8) preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
@ -93,8 +112,6 @@ describe('DetectTokensController', () => {
}) })
it('should trigger detect new tokens when change address', async () => { it('should trigger detect new tokens when change address', async () => {
network.setProviderType('mainnet')
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = true controller.isUnlocked = true
var stub = sandbox.stub(controller, 'detectNewTokens') var stub = sandbox.stub(controller, 'detectNewTokens')
@ -103,8 +120,6 @@ describe('DetectTokensController', () => {
}) })
it('should trigger detect new tokens when submit password', async () => { it('should trigger detect new tokens when submit password', async () => {
network.setProviderType('mainnet')
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.selectedAddress = '0x0' controller.selectedAddress = '0x0'
var stub = sandbox.stub(controller, 'detectNewTokens') var stub = sandbox.stub(controller, 'detectNewTokens')
@ -113,8 +128,6 @@ describe('DetectTokensController', () => {
}) })
it('should not trigger detect new tokens when not open or not unlocked', async () => { it('should not trigger detect new tokens when not open or not unlocked', async () => {
network.setProviderType('mainnet')
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true controller.isOpen = true
controller.isUnlocked = false controller.isUnlocked = false
var stub = sandbox.stub(controller, 'detectTokenBalance') var stub = sandbox.stub(controller, 'detectTokenBalance')
@ -125,4 +138,4 @@ describe('DetectTokensController', () => {
clock.tick(180000) clock.tick(180000)
sandbox.assert.notCalled(stub) sandbox.assert.notCalled(stub)
}) })
}) })

View File

@ -3,9 +3,10 @@ const sinon = require('sinon')
const clone = require('clone') const clone = require('clone')
const nock = require('nock') const nock = require('nock')
const createThoughStream = require('through2').obj const createThoughStream = require('through2').obj
const MetaMaskController = require('../../../../app/scripts/metamask-controller')
const blacklistJSON = require('eth-phishing-detect/src/config') const blacklistJSON = require('eth-phishing-detect/src/config')
const firstTimeState = require('../../../../app/scripts/first-time-state') const MetaMaskController = require('../../../../app/scripts/metamask-controller')
const firstTimeState = require('../../../unit/localhostState')
const createTxMeta = require('../../../lib/createTxMeta')
const currentNetworkId = 42 const currentNetworkId = 42
const DEFAULT_LABEL = 'Account 1' const DEFAULT_LABEL = 'Account 1'
@ -13,6 +14,7 @@ const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm re
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
const CUSTOM_RPC_URL = 'http://localhost:8545'
describe('MetaMaskController', function () { describe('MetaMaskController', function () {
let metamaskController let metamaskController
@ -346,29 +348,19 @@ describe('MetaMaskController', function () {
}) })
describe('#setCustomRpc', function () { describe('#setCustomRpc', function () {
const customRPC = 'https://custom.rpc/'
let rpcTarget let rpcTarget
beforeEach(function () { beforeEach(function () {
rpcTarget = metamaskController.setCustomRpc(CUSTOM_RPC_URL)
nock('https://custom.rpc')
.post('/')
.reply(200)
rpcTarget = metamaskController.setCustomRpc(customRPC)
})
afterEach(function () {
nock.cleanAll()
}) })
it('returns custom RPC that when called', async function () { it('returns custom RPC that when called', async function () {
assert.equal(await rpcTarget, customRPC) assert.equal(await rpcTarget, CUSTOM_RPC_URL)
}) })
it('changes the network controller rpc', function () { it('changes the network controller rpc', function () {
const networkControllerState = metamaskController.networkController.store.getState() const networkControllerState = metamaskController.networkController.store.getState()
assert.equal(networkControllerState.provider.rpcTarget, customRPC) assert.equal(networkControllerState.provider.rpcTarget, CUSTOM_RPC_URL)
}) })
}) })
@ -473,9 +465,10 @@ describe('MetaMaskController', function () {
getNetworkstub.returns(42) getNetworkstub.returns(42)
metamaskController.txController.txStateManager._saveTxList([ metamaskController.txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} }, createTxMeta({ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} }),
{ id: 2, status: 'rejected', metamaskNetworkId: 32, txParams: {} }, createTxMeta({ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} }),
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4'} }, createTxMeta({ id: 2, status: 'rejected', metamaskNetworkId: 32 }),
createTxMeta({ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4'} }),
]) ])
}) })
@ -552,14 +545,14 @@ describe('MetaMaskController', function () {
}) })
describe('#newUnsignedMessage', function () { describe('#newUnsignedMessage', () => {
let msgParams, metamaskMsgs, messages, msgId let msgParams, metamaskMsgs, messages, msgId
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
const data = '0x43727970746f6b697474696573' const data = '0x43727970746f6b697474696573'
beforeEach(async function () { beforeEach(async () => {
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
@ -568,7 +561,10 @@ describe('MetaMaskController', function () {
'data': data, 'data': data,
} }
metamaskController.newUnsignedMessage(msgParams, noop) const promise = metamaskController.newUnsignedMessage(msgParams)
// handle the promise so it doesn't throw an unhandledRejection
promise.then(noop).catch(noop)
metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs() metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs()
messages = metamaskController.messageManager.messages messages = metamaskController.messageManager.messages
msgId = Object.keys(metamaskMsgs)[0] msgId = Object.keys(metamaskMsgs)[0]
@ -608,13 +604,16 @@ describe('MetaMaskController', function () {
describe('#newUnsignedPersonalMessage', function () { describe('#newUnsignedPersonalMessage', function () {
it('errors with no from in msgParams', function () { it('errors with no from in msgParams', async () => {
const msgParams = { const msgParams = {
'data': data, 'data': data,
} }
metamaskController.newUnsignedPersonalMessage(msgParams, function (error) { try {
await metamaskController.newUnsignedPersonalMessage(msgParams)
assert.fail('should have thrown')
} catch (error) {
assert.equal(error.message, 'MetaMask Message Signature: from field is required.') assert.equal(error.message, 'MetaMask Message Signature: from field is required.')
}) }
}) })
let msgParams, metamaskPersonalMsgs, personalMessages, msgId let msgParams, metamaskPersonalMsgs, personalMessages, msgId
@ -631,7 +630,10 @@ describe('MetaMaskController', function () {
'data': data, 'data': data,
} }
metamaskController.newUnsignedPersonalMessage(msgParams, noop) const promise = metamaskController.newUnsignedPersonalMessage(msgParams)
// handle the promise so it doesn't throw an unhandledRejection
promise.then(noop).catch(noop)
metamaskPersonalMsgs = metamaskController.personalMessageManager.getUnapprovedMsgs() metamaskPersonalMsgs = metamaskController.personalMessageManager.getUnapprovedMsgs()
personalMessages = metamaskController.personalMessageManager.messages personalMessages = metamaskController.personalMessageManager.messages
msgId = Object.keys(metamaskPersonalMsgs)[0] msgId = Object.keys(metamaskPersonalMsgs)[0]
@ -670,22 +672,27 @@ describe('MetaMaskController', function () {
describe('#setupUntrustedCommunication', function () { describe('#setupUntrustedCommunication', function () {
let streamTest let streamTest
const phishingUrl = 'decentral.market' const phishingUrl = 'myethereumwalletntw.com'
afterEach(function () { afterEach(function () {
streamTest.end() streamTest.end()
}) })
it('sets up phishing stream for untrusted communication ', async function () { it('sets up phishing stream for untrusted communication ', async () => {
await metamaskController.blacklistController.updatePhishingList() await metamaskController.blacklistController.updatePhishingList()
console.log(blacklistJSON.blacklist.includes(phishingUrl))
const { promise, resolve } = deferredPromise()
streamTest = createThoughStream((chunk, enc, cb) => { streamTest = createThoughStream((chunk, enc, cb) => {
assert.equal(chunk.name, 'phishing') if (chunk.name !== 'phishing') return cb()
assert.equal(chunk.data.hostname, phishingUrl) assert.equal(chunk.data.hostname, phishingUrl)
cb() resolve()
}) cb()
// console.log(streamTest) })
metamaskController.setupUntrustedCommunication(streamTest, phishingUrl) metamaskController.setupUntrustedCommunication(streamTest, phishingUrl)
await promise
}) })
}) })
@ -732,3 +739,9 @@ describe('MetaMaskController', function () {
}) })
}) })
function deferredPromise () {
let resolve
const promise = new Promise(_resolve => { resolve = _resolve })
return { promise, resolve }
}

View File

@ -32,9 +32,10 @@ describe('# Network Controller', function () {
describe('#provider', function () { describe('#provider', function () {
it('provider should be updatable without reassignment', function () { it('provider should be updatable without reassignment', function () {
networkController.initializeProvider(networkControllerProviderConfig) networkController.initializeProvider(networkControllerProviderConfig)
const proxy = networkController._proxy const providerProxy = networkController.getProviderAndBlockTracker().provider
proxy.setTarget({ test: true, on: () => {} }) assert.equal(providerProxy.test, undefined)
assert.ok(proxy.test) providerProxy.setTarget({ test: true })
assert.equal(providerProxy.test, true)
}) })
}) })
describe('#getNetworkState', function () { describe('#getNetworkState', function () {

View File

@ -224,14 +224,15 @@ function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
providerResultStub.result = providerStub providerResultStub.result = providerStub
const provider = { const provider = {
sendAsync: (_, cb) => { cb(undefined, providerResultStub) }, sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
_blockTracker: { }
getCurrentBlock: () => '0x11b568', const blockTracker = {
}, getCurrentBlock: () => '0x11b568',
getLatestBlock: async () => '0x11b568',
} }
return new NonceTracker({ return new NonceTracker({
provider, provider,
blockTracker,
getPendingTransactions, getPendingTransactions,
getConfirmedTransactions, getConfirmedTransactions,
}) })
} }

View File

@ -9,6 +9,7 @@ describe('PendingTransactionTracker', function () {
let pendingTxTracker, txMeta, txMetaNoHash, providerResultStub, let pendingTxTracker, txMeta, txMetaNoHash, providerResultStub,
provider, txMeta3, txList, knownErrors provider, txMeta3, txList, knownErrors
this.timeout(10000) this.timeout(10000)
beforeEach(function () { beforeEach(function () {
txMeta = { txMeta = {
id: 1, id: 1,
@ -40,7 +41,10 @@ describe('PendingTransactionTracker', function () {
getPendingTransactions: () => { return [] }, getPendingTransactions: () => { return [] },
getCompletedTransactions: () => { return [] }, getCompletedTransactions: () => { return [] },
publishTransaction: () => {}, publishTransaction: () => {},
confirmTransaction: () => {},
}) })
pendingTxTracker._getBlock = (blockNumber) => { return {number: blockNumber, transactions: []} }
}) })
describe('_checkPendingTx state management', function () { describe('_checkPendingTx state management', function () {
@ -92,58 +96,6 @@ describe('PendingTransactionTracker', function () {
}) })
}) })
describe('#checkForTxInBlock', function () {
it('should return if no pending transactions', function () {
// throw a type error if it trys to do anything on the block
// thus failing the test
const block = Proxy.revocable({}, {}).revoke()
pendingTxTracker.checkForTxInBlock(block)
})
it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) {
const block = Proxy.revocable({}, {}).revoke()
pendingTxTracker.getPendingTransactions = () => [txMetaNoHash]
pendingTxTracker.once('tx:failed', (txId, err) => {
assert(txId, txMetaNoHash.id, 'should pass txId')
done()
})
pendingTxTracker.checkForTxInBlock(block)
})
it('should emit \'txConfirmed\' if the tx is in the block', function (done) {
const block = { transactions: [txMeta]}
pendingTxTracker.getPendingTransactions = () => [txMeta]
pendingTxTracker.once('tx:confirmed', (txId) => {
assert(txId, txMeta.id, 'should pass txId')
done()
})
pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
pendingTxTracker.checkForTxInBlock(block)
})
})
describe('#queryPendingTxs', function () {
it('should call #_checkPendingTxs if their is no oldBlock', function (done) {
let oldBlock
const newBlock = { number: '0x01' }
pendingTxTracker._checkPendingTxs = done
pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
})
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) {
const oldBlock = { number: '0x01' }
const newBlock = { number: '0x03' }
pendingTxTracker._checkPendingTxs = done
pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
})
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) {
const oldBlock = { number: '0x1' }
const newBlock = { number: '0x2' }
pendingTxTracker._checkPendingTxs = () => {
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less')
done(err)
}
pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
done()
})
})
describe('#_checkPendingTx', function () { describe('#_checkPendingTx', function () {
it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) { it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) {
pendingTxTracker.once('tx:failed', (txId, err) => { pendingTxTracker.once('tx:failed', (txId, err) => {
@ -157,16 +109,6 @@ describe('PendingTransactionTracker', function () {
providerResultStub.eth_getTransactionByHash = null providerResultStub.eth_getTransactionByHash = null
pendingTxTracker._checkPendingTx(txMeta) pendingTxTracker._checkPendingTx(txMeta)
}) })
it('should emit \'txConfirmed\'', function (done) {
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'}
pendingTxTracker.once('tx:confirmed', (txId) => {
assert(txId, txMeta.id, 'should pass txId')
done()
})
pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
pendingTxTracker._checkPendingTx(txMeta)
})
}) })
describe('#_checkPendingTxs', function () { describe('#_checkPendingTxs', function () {
@ -180,19 +122,19 @@ describe('PendingTransactionTracker', function () {
}) })
}) })
it('should warp all txMeta\'s in #_checkPendingTx', function (done) { it('should warp all txMeta\'s in #updatePendingTxs', function (done) {
pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker.getPendingTransactions = () => txList
pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) }
Promise.all(txList.map((tx) => tx.processed)) Promise.all(txList.map((tx) => tx.processed))
.then((txCompletedList) => done()) .then((txCompletedList) => done())
.catch(done) .catch(done)
pendingTxTracker._checkPendingTxs() pendingTxTracker.updatePendingTxs()
}) })
}) })
describe('#resubmitPendingTxs', function () { describe('#resubmitPendingTxs', function () {
const blockStub = { number: '0x0' } const blockNumberStub = '0x0'
beforeEach(function () { beforeEach(function () {
const txMeta2 = txMeta3 = txMeta const txMeta2 = txMeta3 = txMeta
txList = [txMeta, txMeta2, txMeta3].map((tx) => { txList = [txMeta, txMeta2, txMeta3].map((tx) => {
@ -210,7 +152,7 @@ describe('PendingTransactionTracker', function () {
Promise.all(txList.map((tx) => tx.processed)) Promise.all(txList.map((tx) => tx.processed))
.then((txCompletedList) => done()) .then((txCompletedList) => done())
.catch(done) .catch(done)
pendingTxTracker.resubmitPendingTxs(blockStub) pendingTxTracker.resubmitPendingTxs(blockNumberStub)
}) })
it('should not emit \'tx:failed\' if the txMeta throws a known txError', function (done) { it('should not emit \'tx:failed\' if the txMeta throws a known txError', function (done) {
knownErrors = [ knownErrors = [
@ -237,7 +179,7 @@ describe('PendingTransactionTracker', function () {
.then((txCompletedList) => done()) .then((txCompletedList) => done())
.catch(done) .catch(done)
pendingTxTracker.resubmitPendingTxs(blockStub) pendingTxTracker.resubmitPendingTxs(blockNumberStub)
}) })
it('should emit \'tx:warning\' if it encountered a real error', function (done) { it('should emit \'tx:warning\' if it encountered a real error', function (done) {
pendingTxTracker.once('tx:warning', (txMeta, err) => { pendingTxTracker.once('tx:warning', (txMeta, err) => {
@ -255,7 +197,7 @@ describe('PendingTransactionTracker', function () {
.then((txCompletedList) => done()) .then((txCompletedList) => done())
.catch(done) .catch(done)
pendingTxTracker.resubmitPendingTxs(blockStub) pendingTxTracker.resubmitPendingTxs(blockNumberStub)
}) })
}) })
describe('#_resubmitTx', function () { describe('#_resubmitTx', function () {

View File

@ -1,4 +1,5 @@
const assert = require('assert') const assert = require('assert')
const EventEmitter = require('events')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthTx = require('ethereumjs-tx') const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
@ -22,12 +23,14 @@ describe('Transaction Controller', function () {
} }
provider = createTestProviderTools({ scaffold: providerResultStub }).provider provider = createTestProviderTools({ scaffold: providerResultStub }).provider
fromAccount = getTestAccounts()[0] fromAccount = getTestAccounts()[0]
const blockTrackerStub = new EventEmitter()
blockTrackerStub.getCurrentBlock = noop
blockTrackerStub.getLatestBlock = noop
txController = new TransactionController({ txController = new TransactionController({
provider, provider,
networkStore: new ObservableStore(currentNetworkId), networkStore: new ObservableStore(currentNetworkId),
txHistoryLimit: 10, txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, blockTracker: blockTrackerStub,
signTransaction: (ethTx) => new Promise((resolve) => { signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(fromAccount.key) ethTx.sign(fromAccount.key)
resolve() resolve()
@ -49,9 +52,9 @@ describe('Transaction Controller', function () {
describe('#getUnapprovedTxCount', function () { describe('#getUnapprovedTxCount', function () {
it('should return the number of unapproved txs', function () { it('should return the number of unapproved txs', function () {
txController.txStateManager._saveTxList([ txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {}, history: [] },
{ id: 2, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 2, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {}, history: [] },
{ id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {}, history: [] },
]) ])
const unapprovedTxCount = txController.getUnapprovedTxCount() const unapprovedTxCount = txController.getUnapprovedTxCount()
assert.equal(unapprovedTxCount, 3, 'should be 3') assert.equal(unapprovedTxCount, 3, 'should be 3')
@ -61,9 +64,9 @@ describe('Transaction Controller', function () {
describe('#getPendingTxCount', function () { describe('#getPendingTxCount', function () {
it('should return the number of pending txs', function () { it('should return the number of pending txs', function () {
txController.txStateManager._saveTxList([ txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {}, history: [] },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {}, history: [] },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {}, history: [] },
]) ])
const pendingTxCount = txController.getPendingTxCount() const pendingTxCount = txController.getPendingTxCount()
assert.equal(pendingTxCount, 3, 'should be 3') assert.equal(pendingTxCount, 3, 'should be 3')
@ -79,15 +82,15 @@ describe('Transaction Controller', function () {
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
} }
txController.txStateManager._saveTxList([ txController.txStateManager._saveTxList([
{id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, {id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, {id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, {id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams}, {id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams}, {id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams}, {id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams}, {id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams}, {id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [] },
{id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams}, {id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams, history: [] },
]) ])
}) })
@ -201,24 +204,22 @@ describe('Transaction Controller', function () {
}) })
describe('#addTxGasDefaults', function () { describe('#addTxGasDefaults', function () {
it('should add the tx defaults if their are none', function (done) { it('should add the tx defaults if their are none', async () => {
const txMeta = { const txMeta = {
'txParams': { txParams: {
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}, },
history: [],
} }
providerResultStub.eth_gasPrice = '4a817c800' providerResultStub.eth_gasPrice = '4a817c800'
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' } providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
providerResultStub.eth_estimateGas = '5209' providerResultStub.eth_estimateGas = '5209'
txController.addTxGasDefaults(txMeta)
.then((txMetaWithDefaults) => { const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta)
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value') assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price') assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field') assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field')
done()
})
.catch(done)
}) })
}) })
@ -381,8 +382,9 @@ describe('Transaction Controller', function () {
}) })
it('should publish a tx, updates the rawTx when provided a one', async function () { it('should publish a tx, updates the rawTx when provided a one', async function () {
const rawTx = '0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c'
txController.txStateManager.addTx(txMeta) txController.txStateManager.addTx(txMeta)
await txController.publishTransaction(txMeta.id) await txController.publishTransaction(txMeta.id, rawTx)
const publishedTx = txController.txStateManager.getTx(1) const publishedTx = txController.txStateManager.getTx(1)
assert.equal(publishedTx.hash, hash) assert.equal(publishedTx.hash, hash)
assert.equal(publishedTx.status, 'submitted') assert.equal(publishedTx.status, 'submitted')
@ -398,7 +400,7 @@ describe('Transaction Controller', function () {
data: '0x0', data: '0x0',
} }
txController.txStateManager._saveTxList([ txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams }, { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [] },
]) ])
txController.retryTransaction(1) txController.retryTransaction(1)
.then((txMeta) => { .then((txMeta) => {

View File

@ -1,6 +1,3 @@
// polyfill fetch
global.fetch = global.fetch || require('isomorphic-fetch')
const assert = require('assert') const assert = require('assert')
const configManagerGen = require('../lib/mock-config-manager') const configManagerGen = require('../lib/mock-config-manager')

View File

@ -0,0 +1,21 @@
/**
* @typedef {Object} FirstTimeState
* @property {Object} config Initial configuration parameters
* @property {Object} NetworkController Network controller state
*/
/**
* @type {FirstTimeState}
*/
const initialState = {
config: {},
NetworkController: {
provider: {
type: 'rpc',
rpcTarget: 'http://localhost:8545',
},
},
}
module.exports = initialState