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

Merge branch 'master' into RemoveSlackLink

This commit is contained in:
Kevin Serrano 2017-10-02 15:23:27 -07:00 committed by GitHub
commit 4b0e6a0a77
46 changed files with 1792 additions and 1931 deletions

View File

@ -3,7 +3,30 @@
## Current Master ## Current Master
- Remove Slack link from info page, since it is a big phishing target. - Remove Slack link from info page, since it is a big phishing target.
## 3.10.8 2017-9-28
- Fixed usage of new currency fetching API.
## 3.10.7 2017-9-28
- Fixed bug where sometimes the current account was not correctly set and exposed to web apps.
- Added AUD, HKD, SGD, IDR, PHP to currency conversion list
## 3.10.6 2017-9-27
- Fix bug where newly created accounts were not selected.
- Fix bug where selected account was not persisted between lockings.
## 3.10.5 2017-9-27
- Fix block gas limit estimation.
## 3.10.4 2017-9-27
- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) - Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9)
- Fix memory leak warning.
- Fix bug where new event filters would not include historical events.
## 3.10.3 2017-9-21 ## 3.10.3 2017-9-21
@ -23,7 +46,8 @@ rollback to 3.10.0 due to bug
- Fixed a long standing memory leak associated with filters installed by dapps - Fixed a long standing memory leak associated with filters installed by dapps
- Fix link to support center. - Fix link to support center.
- Fixed tooltip icon locations to avoid overflow. - Fixed tooltip icon locations to avoid overflow.
- Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher) - Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher
- Sort currencies by currency name (thanks to strelok1: https://github.com/strelok1).
## 3.10.0 2017-9-11 ## 3.10.0 2017-9-11
@ -33,6 +57,7 @@ rollback to 3.10.0 due to bug
- Add validation preventing users from inputting their own addresses as token tracking addresses. - Add validation preventing users from inputting their own addresses as token tracking addresses.
- Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94) - Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94)
## 3.9.13 2017-9-8 ## 3.9.13 2017-9-8
- Changed the way we initialize the inpage provider to fix a bug affecting some developers. - Changed the way we initialize the inpage provider to fix a bug affecting some developers.

View File

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.10.3", "version": "3.10.8",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
@ -57,7 +57,8 @@
"permissions": [ "permissions": [
"storage", "storage",
"clipboardWrite", "clipboardWrite",
"http://localhost:8545/" "http://localhost:8545/",
"https://*.infura.io/"
], ],
"web_accessible_resources": [ "web_accessible_resources": [
"scripts/inpage.js" "scripts/inpage.js"

View File

@ -114,7 +114,7 @@ function setupController (initState) {
// //
updateBadge() updateBadge()
controller.txController.on('updateBadge', updateBadge) controller.txController.on('update:badge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge) controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge)

View File

@ -2,11 +2,13 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask' const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask' const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
const LOCALHOST_RPC_URL = 'http://localhost:8545'
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
module.exports = { module.exports = {
network: { network: {
localhost: LOCALHOST_RPC_URL,
mainnet: MAINET_RPC_URL, mainnet: MAINET_RPC_URL,
ropsten: ROPSTEN_RPC_URL, ropsten: ROPSTEN_RPC_URL,
kovan: KOVAN_RPC_URL, kovan: KOVAN_RPC_URL,

View File

@ -42,16 +42,21 @@ function setupStreams () {
name: 'contentscript', name: 'contentscript',
target: 'inpage', target: 'inpage',
}) })
pageStream.on('error', console.error)
const pluginPort = extension.runtime.connect({ name: 'contentscript' }) const pluginPort = extension.runtime.connect({ name: 'contentscript' })
const pluginStream = new PortStream(pluginPort) const pluginStream = new PortStream(pluginPort)
pluginStream.on('error', console.error)
// forward communication plugin->inpage // forward communication plugin->inpage
pageStream.pipe(pluginStream).pipe(pageStream) pump(
pageStream,
pluginStream,
pageStream,
(err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err)
)
// setup local multistream channels // setup local multistream channels
const mux = new ObjectMultiplex() const mux = new ObjectMultiplex()
mux.setMaxListeners(25)
pump( pump(
mux, mux,
pageStream, pageStream,

View File

@ -33,9 +33,18 @@ class BalanceController {
_registerUpdates () { _registerUpdates () {
const update = this.updateBalance.bind(this) const update = this.updateBalance.bind(this)
this.txController.on('submitted', update)
this.txController.on('confirmed', update) this.txController.on('tx:status-update', (txId, status) => {
this.txController.on('failed', update) switch (status) {
case 'submitted':
case 'confirmed':
case 'failed':
update()
return
default:
return
}
})
this.accountTracker.store.subscribe(update) this.accountTracker.store.subscribe(update)
this.blockTracker.on('block', update) this.blockTracker.on('block', update)
} }

View File

@ -8,7 +8,7 @@ class CurrencyController {
constructor (opts = {}) { constructor (opts = {}) {
const initState = extend({ const initState = extend({
currentCurrency: 'USD', currentCurrency: 'usd',
conversionRate: 0, conversionRate: 0,
conversionDate: 'N/A', conversionDate: 'N/A',
}, opts.initState) }, opts.initState)
@ -45,10 +45,10 @@ class CurrencyController {
updateConversionRate () { updateConversionRate () {
const currentCurrency = this.getCurrentCurrency() const currentCurrency = this.getCurrentCurrency()
return fetch(`https://api.cryptonator.com/api/ticker/eth-${currentCurrency}`) return fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`)
.then(response => response.json()) .then(response => response.json())
.then((parsedResponse) => { .then((parsedResponse) => {
this.setConversionRate(Number(parsedResponse.ticker.price)) this.setConversionRate(Number(parsedResponse.bid))
this.setConversionDate(Number(parsedResponse.timestamp)) this.setConversionDate(Number(parsedResponse.timestamp))
}).catch((err) => { }).catch((err) => {
if (err) { if (err) {

View File

@ -1,71 +1,40 @@
const assert = require('assert')
const EventEmitter = require('events') const EventEmitter = require('events')
const MetaMaskProvider = require('web3-provider-engine/zero.js') const createMetamaskProvider = require('web3-provider-engine/zero.js')
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 extend = require('xtend')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const createEventEmitterProxy = require('../lib/events-proxy.js')
const RPC_ADDRESS_LIST = require('../config.js').network const RPC_ADDRESS_LIST = require('../config.js').network
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
module.exports = class NetworkController extends EventEmitter { module.exports = class NetworkController extends EventEmitter {
constructor (config) { constructor (config) {
super() super()
this.networkStore = new ObservableStore('loading')
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.networkStore = new ObservableStore('loading')
this.providerStore = new ObservableStore(config.provider) this.providerStore = new ObservableStore(config.provider)
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
this._providerListeners = {} this._proxy = createEventEmitterProxy()
this.on('networkDidChange', this.lookupNetwork) this.on('networkDidChange', this.lookupNetwork)
this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget}))
} }
get provider () { initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams
const rpcUrl = this.getCurrentRpcAddress()
this._configureStandardProvider({ rpcUrl })
this._proxy.on('block', this._logBlock.bind(this))
this._proxy.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this._proxy)
this.lookupNetwork()
return this._proxy return this._proxy
} }
set provider (provider) {
this._provider = provider
}
initializeProvider (opts, providerContructor = MetaMaskProvider) {
this.providerInit = opts
this._provider = providerContructor(opts)
this._proxy = new Proxy(this._provider, {
get: (obj, name) => {
if (name === 'on') return this._on.bind(this)
return this._provider[name]
},
set: (obj, name, value) => {
this._provider[name] = value
return value
},
})
this.provider.on('block', this._logBlock.bind(this))
this.provider.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this.provider)
this.lookupNetwork()
return this.provider
}
switchNetwork (providerInit) {
this.setNetworkState('loading')
const newInit = extend(this.providerInit, providerInit)
this.providerInit = newInit
this._provider.removeAllListeners()
this._provider.stop()
this.provider = MetaMaskProvider(newInit)
// apply the listners created by other controllers
Object.keys(this._providerListeners).forEach((key) => {
this._providerListeners[key].forEach((handler) => this._provider.addListener(key, handler))
})
this.emit('networkDidChange')
}
verifyNetwork () { verifyNetwork () {
// Check network when restoring connectivity: // Check network when restoring connectivity:
if (this.isNetworkLoading()) this.lookupNetwork() if (this.isNetworkLoading()) this.lookupNetwork()
} }
@ -94,6 +63,7 @@ module.exports = class NetworkController extends EventEmitter {
type: 'rpc', type: 'rpc',
rpcTarget: rpcUrl, rpcTarget: rpcUrl,
}) })
this._switchNetwork({ rpcUrl })
} }
getCurrentRpcAddress () { getCurrentRpcAddress () {
@ -102,10 +72,14 @@ module.exports = class NetworkController extends EventEmitter {
return this.getRpcAddressForType(provider.type) return this.getRpcAddressForType(provider.type)
} }
setProviderType (type) { async setProviderType (type) {
assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`)
// skip if type already matches
if (type === this.getProviderConfig().type) return if (type === this.getProviderConfig().type) return
const rpcTarget = this.getRpcAddressForType(type) const rpcTarget = this.getRpcAddressForType(type)
this.providerStore.updateState({type, rpcTarget}) assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`)
this.providerStore.updateState({ type, rpcTarget })
this._switchNetwork({ rpcUrl: rpcTarget })
} }
getProviderConfig () { getProviderConfig () {
@ -117,14 +91,42 @@ module.exports = class NetworkController extends EventEmitter {
return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC
} }
//
// Private
//
_switchNetwork (providerParams) {
this.setNetworkState('loading')
this._configureStandardProvider(providerParams)
this.emit('networkDidChange')
}
_configureStandardProvider(_providerParams) {
const providerParams = extend(this._baseProviderParams, _providerParams)
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
}
_setProvider (provider) {
// collect old block tracker events
const oldProvider = this._provider
let blockTrackerHandlers
if (oldProvider) {
// capture old block handlers
blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers
// tear down
oldProvider.removeAllListeners()
oldProvider.stop()
}
// override block tracler
provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers)
// set as new provider
this._provider = provider
this._proxy.setTarget(provider)
}
_logBlock (block) { _logBlock (block) {
log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
this.verifyNetwork() this.verifyNetwork()
} }
_on (event, handler) {
if (!this._providerListeners[event]) this._providerListeners[event] = []
this._providerListeners[event].push(handler)
this._provider.on(event, handler)
}
} }

View File

@ -22,7 +22,7 @@ class PreferencesController {
}) })
} }
getSelectedAddress (_address) { getSelectedAddress () {
return this.store.getState().selectedAddress return this.store.getState().selectedAddress
} }

View File

@ -1,86 +1,90 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const extend = require('xtend')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const TxProviderUtil = require('../lib/tx-utils') const TransactionStateManger = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker') const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id') const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('../lib/nonce-tracker')
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
/*
Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller
- txStateManager
responsible for the state of a transaction and
storing the transaction
- pendingTxTracker
watching blocks for transactions to be include
and emitting confirmed events
- txGasUtil
gas calculations and safety buffering
- nonceTracker
calculating nonces
*/
module.exports = class TransactionController extends EventEmitter { module.exports = class TransactionController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.store = new ObservableStore(extend({
transactions: [],
}, opts.initState))
this.memStore = new ObservableStore({})
this.networkStore = opts.networkStore || new ObservableStore({}) this.networkStore = opts.networkStore || new ObservableStore({})
this.preferencesStore = opts.preferencesStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({})
this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider this.provider = opts.provider
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction this.signEthTx = opts.signTransaction
this.accountTracker = opts.accountTracker
this.memStore = new ObservableStore({})
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
this.txStateManager = new TransactionStateManger({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),
})
this.store = this.txStateManager.store
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.nonceTracker = new NonceTracker({ this.nonceTracker = new NonceTracker({
provider: this.provider, provider: this.provider,
getPendingTransactions: (address) => { getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
return this.getFilteredTxList({
from: address,
status: 'submitted',
err: undefined,
})
},
getConfirmedTransactions: (address) => { getConfirmedTransactions: (address) => {
return this.getFilteredTxList({ return this.txStateManager.getFilteredTxList({
from: address, from: address,
status: 'confirmed', status: 'confirmed',
err: undefined, err: undefined,
}) })
}, },
giveUpOnTransaction: (txId) => {
const msg = `Gave up submitting after 3500 blocks un-mined.`
this.setTxStatusFailed(txId, msg)
},
}) })
this.query = new EthQuery(this.provider)
this.txProviderUtil = new TxProviderUtil(this.provider)
this.pendingTxTracker = new PendingTransactionTracker({ this.pendingTxTracker = new PendingTransactionTracker({
provider: this.provider, provider: this.provider,
nonceTracker: this.nonceTracker, nonceTracker: this.nonceTracker,
getBalance: (address) => { retryLimit: 3500, // Retry 3500 blocks, or about 1 day.
const account = this.accountTracker.getState().accounts[address] publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
if (!account) return getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
return account.balance
},
publishTransaction: this.txProviderUtil.publishTransaction.bind(this.txProviderUtil),
getPendingTransactions: () => {
const network = this.getNetwork()
return this.getFilteredTxList({
status: 'submitted',
metamaskNetworkId: network,
})
},
}) })
this.pendingTxTracker.on('txWarning', this.updateTx.bind(this)) this.txStateManager.store.subscribe(() => this.emit('update:badge'))
this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this))
this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this))
this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either // this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition // removed or redone this is to guard against the race condition
// where accountTracker hasent been populated by the results yet this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
this.blockTracker.once('latest', () => {
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
})
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
// memstore is computed from a few different stores // memstore is computed from a few different stores
this._updateMemstore() this._updateMemstore()
this.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())
} }
@ -97,98 +101,31 @@ module.exports = class TransactionController extends EventEmitter {
return this.preferencesStore.getState().selectedAddress return this.preferencesStore.getState().selectedAddress
} }
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
// Returns the full tx list across all networks
getFullTxList () {
return this.store.getState().transactions
}
getUnapprovedTxCount () { getUnapprovedTxCount () {
return Object.keys(this.getUnapprovedTxList()).length return Object.keys(this.txStateManager.getUnapprovedTxList()).length
} }
getPendingTxCount () { getPendingTxCount (account) {
return this.getTxsByMetaData('status', 'signed').length return this.txStateManager.getPendingTransactions(account).length
} }
// Returns the tx list getFilteredTxList (opts) {
getTxList () { return this.txStateManager.getFilteredTxList(opts)
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList)
} }
// gets tx by Id and returns it getChainId () {
getTx (txId) { const networkState = this.networkStore.getState()
const txList = this.getTxList() const getChainId = parseInt(networkState)
const txMeta = txList.find(txData => txData.id === txId) if (Number.isNaN(getChainId)) {
return txMeta return 0
} } else {
getUnapprovedTxList () { return getChainId
const txList = this.getTxList() }
return txList.filter((txMeta) => txMeta.status === 'unapproved')
.reduce((result, tx) => {
result[tx.id] = tx
return result
}, {})
}
updateTx (txMeta) {
// create txMeta snapshot for history
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
// recover previous tx state obj
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
// generate history entry and add to history
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState)
txMeta.history.push(entry)
// commit txMeta to state
const txId = txMeta.id
const txList = this.getFullTxList()
const index = txList.findIndex(txData => txData.id === txId)
txList[index] = txMeta
this._saveTxList(txList)
this.emit('update')
} }
// Adds a tx to the txlist // Adds a tx to the txlist
addTx (txMeta) { addTx (txMeta) {
// initialize history this.txStateManager.addTx(txMeta)
txMeta.history = []
// capture initial snapshot of txMeta for history
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history.push(snapshot)
// checks if the length of the tx history is
// longer then desired persistence limit
// and then if it is removes only confirmed
// or rejected tx's.
// not tx's that are pending or unapproved
const txCount = this.getTxCount()
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
const txHistoryLimit = this.txHistoryLimit
if (txCount > txHistoryLimit - 1) {
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
fullTxList.splice(index, 1)
}
fullTxList.push(txMeta)
this._saveTxList(fullTxList)
this.emit('update')
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
})
this.once(`${txMeta.id}:rejected`, function (txId) {
this.removeAllListeners(`${txMeta.id}:signed`)
})
this.emit('updateBadge')
this.emit(`${txMeta.id}:unapproved`, txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta)
} }
@ -198,7 +135,7 @@ module.exports = class TransactionController extends EventEmitter {
this.emit('newUnaprovedTx', txMeta) this.emit('newUnaprovedTx', txMeta)
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.once(`${txMeta.id}:finished`, (completedTx) => { this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
switch (completedTx.status) { switch (completedTx.status) {
case 'submitted': case 'submitted':
return resolve(completedTx.hash) return resolve(completedTx.hash)
@ -213,7 +150,7 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) { async addUnapprovedTransaction (txParams) {
// validate // validate
await this.txProviderUtil.validateTxParams(txParams) await this.txGasUtil.validateTxParams(txParams)
// construct txMeta // construct txMeta
const txMeta = { const txMeta = {
id: createId(), id: createId(),
@ -232,17 +169,15 @@ module.exports = class TransactionController extends EventEmitter {
async addTxDefaults (txMeta) { async addTxDefaults (txMeta) {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// ensure value // ensure value
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0' txParams.value = txParams.value || '0x0'
if (!txParams.gasPrice) {
const gasPrice = await this.query.gasPrice()
txParams.gasPrice = gasPrice
}
// set gasLimit // set gasLimit
return await this.txProviderUtil.analyzeGasUsage(txMeta) return await this.txGasUtil.analyzeGasUsage(txMeta)
} }
async updateAndApproveTransaction (txMeta) { async updateAndApproveTransaction (txMeta) {
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id) await this.approveTransaction(txMeta.id)
} }
@ -250,24 +185,24 @@ module.exports = class TransactionController extends EventEmitter {
let nonceLock let nonceLock
try { try {
// approve // approve
this.setTxStatusApproved(txId) this.txStateManager.setTxStatusApproved(txId)
// get next nonce // get next nonce
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const fromAddress = txMeta.txParams.from const fromAddress = txMeta.txParams.from
// wait for a nonce // wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress) nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams // add nonce to txParams
txMeta.txParams.nonce = nonceLock.nextNonce txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16))
// add nonce debugging information to txMeta // add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails txMeta.nonceDetails = nonceLock.nonceDetails
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction')
// sign transaction // sign transaction
const rawTx = await this.signTransaction(txId) const rawTx = await this.signTransaction(txId)
await this.publishTransaction(txId, rawTx) await this.publishTransaction(txId, rawTx)
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock() nonceLock.releaseLock()
} catch (err) { } catch (err) {
this.setTxStatusFailed(txId, err) this.txStateManager.setTxStatusFailed(txId, err)
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock() if (nonceLock) nonceLock.releaseLock()
// continue with error chain // continue with error chain
@ -276,181 +211,46 @@ module.exports = class TransactionController extends EventEmitter {
} }
async signTransaction (txId) { async signTransaction (txId) {
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const txParams = txMeta.txParams const txParams = txMeta.txParams
const fromAddress = txParams.from const fromAddress = txParams.from
// add network/chain id // add network/chain id
txParams.chainId = this.getChainId() txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16))
const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams) const ethTx = new Transaction(txParams)
await this.signEthTx(ethTx, fromAddress) await this.signEthTx(ethTx, fromAddress)
this.setTxStatusSigned(txMeta.id) this.txStateManager.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize()) const rawTx = ethUtil.bufferToHex(ethTx.serialize())
return rawTx return rawTx
} }
async publishTransaction (txId, rawTx) { async publishTransaction (txId, rawTx) {
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx txMeta.rawTx = rawTx
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction')
const txHash = await this.txProviderUtil.publishTransaction(rawTx) const txHash = await this.query.sendRawTransaction(rawTx)
this.setTxHash(txId, txHash) this.setTxHash(txId, txHash)
this.setTxStatusSubmitted(txId) this.txStateManager.setTxStatusSubmitted(txId)
} }
async cancelTransaction (txId) { async cancelTransaction (txId) {
this.setTxStatusRejected(txId) this.txStateManager.setTxStatusRejected(txId)
}
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState)
if (Number.isNaN(getChainId)) {
return 0
} else {
return getChainId
}
} }
// receives a txHash records the tx as signed // receives a txHash records the tx as signed
setTxHash (txId, txHash) { setTxHash (txId, txHash) {
// Add the tx hash to the persisted meta-tx object // Add the tx hash to the persisted meta-tx object
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.hash = txHash txMeta.hash = txHash
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta, 'transactions#setTxHash')
} }
/* //
Takes an object of fields to search for eg: // PRIVATE METHODS
let thingsToLookFor = { //
to: '0x0..',
from: '0x0..',
status: 'signed',
err: undefined,
}
and returns a list of tx with all
options matching
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
| so you can also search txs that |
| dont have something as well by |
| setting the value as undefined |
************************************
this is for things like filtering a the tx list
for only tx's from 1 account
or for filltering for all txs from one account
and that have been 'confirmed'
*/
getFilteredTxList (opts) {
let filteredTxList
Object.keys(opts).forEach((key) => {
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
})
return filteredTxList
}
getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => {
if (txMeta.txParams[key]) {
return txMeta.txParams[key] === value
} else {
return txMeta[key] === value
}
})
}
// STATUS METHODS
// get::set status
// should return the status of the tx.
getTxStatus (txId) {
const txMeta = this.getTx(txId)
return txMeta.status
}
// should update the status of the tx to 'rejected'.
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
}
// should update the status of the tx to 'approved'.
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
}
// should update the status of the tx to 'signed'.
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
}
// should update the status of the tx to 'submitted'.
setTxStatusSubmitted (txId) {
this._setTxStatus(txId, 'submitted')
}
// should update the status of the tx to 'confirmed'.
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
}
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
stack: err.stack,
}
this.updateTx(txMeta)
this._setTxStatus(txId, 'failed')
}
// merges txParams obj onto txData.txParams
// use extend to ensure that all fields are filled
updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams)
this.updateTx(txMeta)
}
/* _____________________________________
| |
| PRIVATE METHODS |
|______________________________________*/
// Should find the tx in the tx list and
// update it.
// should set the status in txData
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
_setTxStatus (txId, status) {
const txMeta = this.getTx(txId)
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`${status}`, txId)
if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta)
this.emit('updateBadge')
}
// Saves the new/updated txList.
// Function is intended only for internal use
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
_updateMemstore () { _updateMemstore () {
const unapprovedTxs = this.getUnapprovedTxList() const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.getFilteredTxList({ const selectedAddressTxList = this.txStateManager.getFilteredTxList({
from: this.getSelectedAddress(), from: this.getSelectedAddress(),
metamaskNetworkId: this.getNetwork(), metamaskNetworkId: this.getNetwork(),
}) })

View File

@ -1,596 +0,0 @@
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const bip39 = require('bip39')
const EventEmitter = require('events').EventEmitter
const ObservableStore = require('obs-store')
const filter = require('promise-filter')
const encryptor = require('browser-passworder')
const sigUtil = require('eth-sig-util')
const normalizeAddress = sigUtil.normalize
// Keyrings:
const SimpleKeyring = require('eth-simple-keyring')
const HdKeyring = require('eth-hd-keyring')
const keyringTypes = [
SimpleKeyring,
HdKeyring,
]
class KeyringController extends EventEmitter {
// PUBLIC METHODS
//
// THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
// MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
//
// THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.
constructor (opts) {
super()
const initState = opts.initState || {}
this.keyringTypes = keyringTypes
this.store = new ObservableStore(initState)
this.memStore = new ObservableStore({
isUnlocked: false,
keyringTypes: this.keyringTypes.map(krt => krt.type),
keyrings: [],
identities: {},
})
this.accountTracker = opts.accountTracker
this.encryptor = opts.encryptor || encryptor
this.keyrings = []
this.getNetwork = opts.getNetwork
}
// Full Update
// returns Promise( @object state )
//
// Emits the `update` event and
// returns a Promise that resolves to the current state.
//
// Frequently used to end asynchronous chains in this class,
// indicating consumers can often either listen for updates,
// or accept a state-resolving promise to consume their results.
//
// Not all methods end with this, that might be a nice refactor.
fullUpdate () {
this.emit('update')
return Promise.resolve(this.memStore.getState())
}
// Create New Vault And Keychain
// @string password - The password to encrypt the vault with
//
// returns Promise( @object state )
//
// Destroys any old encrypted storage,
// creates a new encrypted store with the given password,
// randomly creates a new HD wallet with 1 account,
// faucets that account on the testnet.
createNewVaultAndKeychain (password) {
return this.persistAllKeyrings(password)
.then(this.createFirstKeyTree.bind(this))
.then(this.fullUpdate.bind(this))
}
// CreateNewVaultAndRestore
// @string password - The password to encrypt the vault with
// @string seed - The BIP44-compliant seed phrase.
//
// returns Promise( @object state )
//
// Destroys any old encrypted storage,
// creates a new encrypted store with the given password,
// creates a new HD wallet from the given seed with 1 account.
createNewVaultAndRestore (password, seed) {
if (typeof password !== 'string') {
return Promise.reject('Password must be text.')
}
if (!bip39.validateMnemonic(seed)) {
return Promise.reject(new Error('Seed phrase is invalid.'))
}
this.clearKeyrings()
return this.persistAllKeyrings(password)
.then(() => {
return this.addNewKeyring('HD Key Tree', {
mnemonic: seed,
numberOfAccounts: 1,
})
})
.then((firstKeyring) => {
return firstKeyring.getAccounts()
})
.then((accounts) => {
const firstAccount = accounts[0]
if (!firstAccount) throw new Error('KeyringController - First Account not found.')
const hexAccount = normalizeAddress(firstAccount)
this.emit('newAccount', hexAccount)
return this.setupAccounts(accounts)
})
.then(this.persistAllKeyrings.bind(this, password))
.then(this.fullUpdate.bind(this))
}
// Set Locked
// returns Promise( @object state )
//
// This method deallocates all secrets, and effectively locks metamask.
setLocked () {
// set locked
this.password = null
this.memStore.updateState({ isUnlocked: false })
// remove keyrings
this.keyrings = []
this._updateMemStoreKeyrings()
return this.fullUpdate()
}
// Submit Password
// @string password
//
// returns Promise( @object state )
//
// Attempts to decrypt the current vault and load its keyrings
// into memory.
//
// Temporarily also migrates any old-style vaults first, as well.
// (Pre MetaMask 3.0.0)
submitPassword (password) {
return this.unlockKeyrings(password)
.then((keyrings) => {
this.keyrings = keyrings
return this.fullUpdate()
})
}
// Add New Keyring
// @string type
// @object opts
//
// returns Promise( @Keyring keyring )
//
// Adds a new Keyring of the given `type` to the vault
// and the current decrypted Keyrings array.
//
// All Keyring classes implement a unique `type` string,
// and this is used to retrieve them from the keyringTypes array.
addNewKeyring (type, opts) {
const Keyring = this.getKeyringClassForType(type)
const keyring = new Keyring(opts)
return keyring.deserialize(opts)
.then(() => {
return keyring.getAccounts()
})
.then((accounts) => {
return this.checkForDuplicate(type, accounts)
})
.then((checkedAccounts) => {
this.keyrings.push(keyring)
return this.setupAccounts(checkedAccounts)
})
.then(() => this.persistAllKeyrings())
.then(() => this._updateMemStoreKeyrings())
.then(() => this.fullUpdate())
.then(() => {
return keyring
})
}
// For now just checks for simple key pairs
// but in the future
// should possibly add HD and other types
//
checkForDuplicate (type, newAccount) {
return this.getAccounts()
.then((accounts) => {
switch (type) {
case 'Simple Key Pair':
const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0]))
return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate'))
default:
return Promise.resolve(newAccount)
}
})
}
// Add New Account
// @number keyRingNum
//
// returns Promise( @object state )
//
// Calls the `addAccounts` method on the Keyring
// in the kryings array at index `keyringNum`,
// and then saves those changes.
addNewAccount (selectedKeyring) {
return selectedKeyring.addAccounts(1)
.then(this.setupAccounts.bind(this))
.then(this.persistAllKeyrings.bind(this))
.then(this._updateMemStoreKeyrings.bind(this))
.then(this.fullUpdate.bind(this))
}
// Save Account Label
// @string account
// @string label
//
// returns Promise( @string label )
//
// Persists a nickname equal to `label` for the specified account.
saveAccountLabel (account, label) {
try {
const hexAddress = normalizeAddress(account)
// update state on diskStore
const state = this.store.getState()
const walletNicknames = state.walletNicknames || {}
walletNicknames[hexAddress] = label
this.store.updateState({ walletNicknames })
// update state on memStore
const identities = this.memStore.getState().identities
identities[hexAddress].name = label
this.memStore.updateState({ identities })
return Promise.resolve(label)
} catch (err) {
return Promise.reject(err)
}
}
// Export Account
// @string address
//
// returns Promise( @string privateKey )
//
// Requests the private key from the keyring controlling
// the specified address.
//
// Returns a Promise that may resolve with the private key string.
exportAccount (address) {
try {
return this.getKeyringForAccount(address)
.then((keyring) => {
return keyring.exportAccount(normalizeAddress(address))
})
} catch (e) {
return Promise.reject(e)
}
}
// SIGNING METHODS
//
// This method signs tx and returns a promise for
// TX Manager to update the state after signing
signTransaction (ethTx, _fromAddress) {
const fromAddress = normalizeAddress(_fromAddress)
return this.getKeyringForAccount(fromAddress)
.then((keyring) => {
return keyring.signTransaction(fromAddress, ethTx)
})
}
// Sign Message
// @object msgParams
//
// returns Promise(@buffer rawSig)
//
// Attempts to sign the provided @object msgParams.
signMessage (msgParams) {
const address = normalizeAddress(msgParams.from)
return this.getKeyringForAccount(address)
.then((keyring) => {
return keyring.signMessage(address, msgParams.data)
})
}
// Sign Personal Message
// @object msgParams
//
// returns Promise(@buffer rawSig)
//
// Attempts to sign the provided @object msgParams.
// Prefixes the hash before signing as per the new geth behavior.
signPersonalMessage (msgParams) {
const address = normalizeAddress(msgParams.from)
return this.getKeyringForAccount(address)
.then((keyring) => {
return keyring.signPersonalMessage(address, msgParams.data)
})
}
// PRIVATE METHODS
//
// THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
// AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
// Create First Key Tree
// returns @Promise
//
// Clears the vault,
// creates a new one,
// creates a random new HD Keyring with 1 account,
// makes that account the selected account,
// faucets that account on testnet,
// puts the current seed words into the state tree.
createFirstKeyTree () {
this.clearKeyrings()
return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
.then((keyring) => {
return keyring.getAccounts()
})
.then((accounts) => {
const firstAccount = accounts[0]
if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
const hexAccount = normalizeAddress(firstAccount)
this.emit('newAccount', hexAccount)
this.emit('newVault', hexAccount)
return this.setupAccounts(accounts)
})
.then(this.persistAllKeyrings.bind(this))
}
// Setup Accounts
// @array accounts
//
// returns @Promise(@object account)
//
// Initializes the provided account array
// Gives them numerically incremented nicknames,
// and adds them to the accountTracker for regular balance checking.
setupAccounts (accounts) {
return this.getAccounts()
.then((loadedAccounts) => {
const arr = accounts || loadedAccounts
return Promise.all(arr.map((account) => {
return this.getBalanceAndNickname(account)
}))
})
}
// Get Balance And Nickname
// @string account
//
// returns Promise( @string label )
//
// Takes an account address and an iterator representing
// the current number of named accounts.
getBalanceAndNickname (account) {
if (!account) {
throw new Error('Problem loading account.')
}
const address = normalizeAddress(account)
this.accountTracker.addAccount(address)
return this.createNickname(address)
}
// Create Nickname
// @string address
//
// returns Promise( @string label )
//
// Takes an address, and assigns it an incremented nickname, persisting it.
createNickname (address) {
const hexAddress = normalizeAddress(address)
const identities = this.memStore.getState().identities
const currentIdentityCount = Object.keys(identities).length + 1
const nicknames = this.store.getState().walletNicknames || {}
const existingNickname = nicknames[hexAddress]
const name = existingNickname || `Account ${currentIdentityCount}`
identities[hexAddress] = {
address: hexAddress,
name,
}
this.memStore.updateState({ identities })
return this.saveAccountLabel(hexAddress, name)
}
// Persist All Keyrings
// @password string
//
// returns Promise
//
// Iterates the current `keyrings` array,
// serializes each one into a serialized array,
// encrypts that array with the provided `password`,
// and persists that encrypted string to storage.
persistAllKeyrings (password = this.password) {
if (typeof password === 'string') {
this.password = password
this.memStore.updateState({ isUnlocked: true })
}
return Promise.all(this.keyrings.map((keyring) => {
return Promise.all([keyring.type, keyring.serialize()])
.then((serializedKeyringArray) => {
// Label the output values on each serialized Keyring:
return {
type: serializedKeyringArray[0],
data: serializedKeyringArray[1],
}
})
}))
.then((serializedKeyrings) => {
return this.encryptor.encrypt(this.password, serializedKeyrings)
})
.then((encryptedString) => {
this.store.updateState({ vault: encryptedString })
return true
})
}
// Unlock Keyrings
// @string password
//
// returns Promise( @array keyrings )
//
// Attempts to unlock the persisted encrypted storage,
// initializing the persisted keyrings to RAM.
unlockKeyrings (password) {
const encryptedVault = this.store.getState().vault
if (!encryptedVault) {
throw new Error('Cannot unlock without a previous vault.')
}
return this.encryptor.decrypt(password, encryptedVault)
.then((vault) => {
this.password = password
this.memStore.updateState({ isUnlocked: true })
vault.forEach(this.restoreKeyring.bind(this))
return this.keyrings
})
}
// Restore Keyring
// @object serialized
//
// returns Promise( @Keyring deserialized )
//
// Attempts to initialize a new keyring from the provided
// serialized payload.
//
// On success, returns the resulting @Keyring instance.
restoreKeyring (serialized) {
const { type, data } = serialized
const Keyring = this.getKeyringClassForType(type)
const keyring = new Keyring()
return keyring.deserialize(data)
.then(() => {
return keyring.getAccounts()
})
.then((accounts) => {
return this.setupAccounts(accounts)
})
.then(() => {
this.keyrings.push(keyring)
this._updateMemStoreKeyrings()
return keyring
})
}
// Get Keyring Class For Type
// @string type
//
// Returns @class Keyring
//
// Searches the current `keyringTypes` array
// for a Keyring class whose unique `type` property
// matches the provided `type`,
// returning it if it exists.
getKeyringClassForType (type) {
return this.keyringTypes.find(kr => kr.type === type)
}
getKeyringsByType (type) {
return this.keyrings.filter((keyring) => keyring.type === type)
}
// Get Accounts
// returns Promise( @Array[ @string accounts ] )
//
// Returns the public addresses of all current accounts
// managed by all currently unlocked keyrings.
getAccounts () {
const keyrings = this.keyrings || []
return Promise.all(keyrings.map(kr => kr.getAccounts()))
.then((keyringArrays) => {
return keyringArrays.reduce((res, arr) => {
return res.concat(arr)
}, [])
})
}
// Get Keyring For Account
// @string address
//
// returns Promise(@Keyring keyring)
//
// Returns the currently initialized keyring that manages
// the specified `address` if one exists.
getKeyringForAccount (address) {
const hexed = normalizeAddress(address)
log.debug(`KeyringController - getKeyringForAccount: ${hexed}`)
return Promise.all(this.keyrings.map((keyring) => {
return Promise.all([
keyring,
keyring.getAccounts(),
])
}))
.then(filter((candidate) => {
const accounts = candidate[1].map(normalizeAddress)
return accounts.includes(hexed)
}))
.then((winners) => {
if (winners && winners.length > 0) {
return winners[0][0]
} else {
throw new Error('No keyring found for the requested account.')
}
})
}
// Display For Keyring
// @Keyring keyring
//
// returns Promise( @Object { type:String, accounts:Array } )
//
// Is used for adding the current keyrings to the state object.
displayForKeyring (keyring) {
return keyring.getAccounts()
.then((accounts) => {
return {
type: keyring.type,
accounts: accounts,
}
})
}
// Add Gas Buffer
// @string gas (as hexadecimal value)
//
// returns @string bufferedGas (as hexadecimal value)
//
// Adds a healthy buffer of gas to an initial gas estimate.
addGasBuffer (gas) {
const gasBuffer = new BN('100000', 10)
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
const correct = bnGas.add(gasBuffer)
return ethUtil.addHexPrefix(correct.toString(16))
}
// Clear Keyrings
//
// Deallocates all currently managed keyrings and accounts.
// Used before initializing a new vault.
clearKeyrings () {
let accounts
try {
accounts = Object.keys(this.accountTracker.getState())
} catch (e) {
accounts = []
}
accounts.forEach((address) => {
this.accountTracker.removeAccount(address)
})
// clear keyrings from memory
this.keyrings = []
this.memStore.updateState({
keyrings: [],
identities: {},
})
}
_updateMemStoreKeyrings () {
Promise.all(this.keyrings.map(this.displayForKeyring))
.then((keyrings) => {
this.memStore.updateState({ keyrings })
})
}
}
module.exports = KeyringController

View File

@ -57,10 +57,10 @@ class AccountTracker extends EventEmitter {
// //
_updateForBlock (block) { _updateForBlock (block) {
const blockNumber = '0x' + block.number.toString('hex') this._currentBlockNumber = block.number
this._currentBlockNumber = blockNumber const currentBlockGasLimit = block.gasLimit
this.store.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) this.store.updateState({ currentBlockGasLimit })
async.parallel([ async.parallel([
this._updateAccounts.bind(this), this._updateAccounts.bind(this),

View File

@ -0,0 +1,31 @@
module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
let target = eventEmitter
const eventHandlers = listeners || {}
const proxy = new Proxy({}, {
get: (obj, name) => {
// intercept listeners
if (name === 'on') return addListener
if (name === 'setTarget') return setTarget
if (name === 'proxyEventHandlers') return eventHandlers
return target[name]
},
set: (obj, name, value) => {
target[name] = value
return true
},
})
function setTarget (eventEmitter) {
target = eventEmitter
// migrate listeners
Object.keys(eventHandlers).forEach((name) => {
eventHandlers[name].forEach((handler) => target.on(name, 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

@ -1,7 +1,5 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const sufficientBalance = require('./util').sufficientBalance
const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
/* /*
Utility class for tracking the transactions as they Utility class for tracking the transactions as they
@ -13,7 +11,6 @@ const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
requires a: { requires a: {
provider: //, provider: //,
nonceTracker: //see nonce tracker, nonceTracker: //see nonce tracker,
getBalnce: //(address) a function for getting balances,
getPendingTransactions: //() a function for getting an array of transactions, getPendingTransactions: //() a function for getting an array of transactions,
publishTransaction: //(rawTx) a async function for publishing raw transactions, publishTransaction: //(rawTx) a async function for publishing raw transactions,
} }
@ -25,11 +22,9 @@ module.exports = 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
this.retryLimit = config.retryLimit || Infinity
this.getBalance = config.getBalance
this.getPendingTransactions = config.getPendingTransactions this.getPendingTransactions = config.getPendingTransactions
this.publishTransaction = config.publishTransaction this.publishTransaction = config.publishTransaction
this.giveUpOnTransaction = config.giveUpOnTransaction
} }
// checks if a signed tx is in a block and // checks if a signed tx is in a block and
@ -44,18 +39,18 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (!txHash) { if (!txHash) {
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
noTxHashErr.name = 'NoTxHashError' noTxHashErr.name = 'NoTxHashError'
this.emit('txFailed', txId, noTxHashErr) this.emit('tx:failed', txId, noTxHashErr)
return return
} }
block.transactions.forEach((tx) => { block.transactions.forEach((tx) => {
if (tx.hash === txHash) this.emit('txConfirmed', txId) if (tx.hash === txHash) this.emit('tx:confirmed', txId)
}) })
}) })
} }
queryPendingTxs ({oldBlock, newBlock}) { queryPendingTxs ({ oldBlock, newBlock }) {
// check pending transactions on start // check pending transactions on start
if (!oldBlock) { if (!oldBlock) {
this._checkPendingTxs() this._checkPendingTxs()
@ -96,26 +91,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
// ignore resubmit warnings, return early // ignore resubmit warnings, return early
if (isKnownTx) return if (isKnownTx) return
// encountered real error - transition to error state // encountered real error - transition to error state
this.emit('txFailed', txMeta.id, err) this.emit('tx:failed', txMeta.id, err)
})) }))
} }
async _resubmitTx (txMeta) { async _resubmitTx (txMeta) {
const address = txMeta.txParams.from if (txMeta.retryCount > this.retryLimit) {
const balance = this.getBalance(address) const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`)
if (balance === undefined) return return this.emit('tx:failed', txMeta.id, err)
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
if (txMeta.retryCount > RETRY_LIMIT) {
return this.giveUpOnTransaction(txMeta.id)
}
// if the value of the transaction is greater then the balance, fail.
if (!sufficientBalance(txMeta.txParams, balance)) {
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
this.emit('txFailed', txMeta.id, insufficientFundsError)
log.error(insufficientFundsError)
return
} }
// Only auto-submit already-signed txs: // Only auto-submit already-signed txs:
@ -125,7 +108,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
const txHash = await this.publishTransaction(rawTx) const txHash = await this.publishTransaction(rawTx)
// Increment successful tries: // Increment successful tries:
txMeta.retryCount++ this.emit('tx:retry', txMeta)
return txHash return txHash
} }
@ -137,7 +120,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (!txHash) { if (!txHash) {
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
noTxHashErr.name = 'NoTxHashError' noTxHashErr.name = 'NoTxHashError'
this.emit('txFailed', txId, noTxHashErr) this.emit('tx:failed', txId, noTxHashErr)
return return
} }
// get latest transaction status // get latest transaction status
@ -146,14 +129,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
txParams = await this.query.getTransactionByHash(txHash) txParams = await this.query.getTransactionByHash(txHash)
if (!txParams) return if (!txParams) return
if (txParams.blockNumber) { if (txParams.blockNumber) {
this.emit('txConfirmed', txId) this.emit('tx:confirmed', txId)
} }
} catch (err) { } catch (err) {
txMeta.warning = { txMeta.warning = {
error: err, error: err,
message: 'There was a problem loading this transaction.', message: 'There was a problem loading this transaction.',
} }
this.emit('txWarning', txMeta) this.emit('tx:warning', txMeta)
throw err throw err
} }
} }

View File

@ -1,6 +1,4 @@
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const Transaction = require('ethereumjs-tx')
const normalize = require('eth-sig-util').normalize
const { const {
hexToBn, hexToBn,
BnMultiplyByFraction, BnMultiplyByFraction,
@ -78,26 +76,6 @@ module.exports = class txProvideUtil {
return bnToHex(upperGasLimitBn) return bnToHex(upperGasLimitBn)
} }
// builds ethTx from txParams object
buildEthTxFromParams (txParams) {
// normalize values
txParams.to = normalize(txParams.to)
txParams.from = normalize(txParams.from)
txParams.value = normalize(txParams.value)
txParams.data = normalize(txParams.data)
txParams.gas = normalize(txParams.gas || txParams.gasLimit)
txParams.gasPrice = normalize(txParams.gasPrice)
txParams.nonce = normalize(txParams.nonce)
// build ethTx
log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`)
const ethTx = new Transaction(txParams)
return ethTx
}
async publishTransaction (rawTx) {
return await this.query.sendRawTransaction(rawTx)
}
async validateTxParams (txParams) { async validateTxParams (txParams) {
if (('value' in txParams) && txParams.value.indexOf('-') === 0) { if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)

View File

@ -20,11 +20,15 @@ function migrateFromSnapshotsToDiffs(longHistory) {
) )
} }
function generateHistoryEntry(previousState, newState) { function generateHistoryEntry(previousState, newState, note) {
return jsonDiffer.compare(previousState, newState) const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry
if (note && entry[0]) entry[0].note = note
return entry
} }
function replayHistory(shortHistory) { function replayHistory(_shortHistory) {
const shortHistory = clone(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
} }

View File

@ -0,0 +1,245 @@
const extend = require('xtend')
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
module.exports = class TransactionStateManger extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()
this.store = new ObservableStore(
extend({
transactions: [],
}, initState))
this.txHistoryLimit = txHistoryLimit
this.getNetwork = getNetwork
}
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
getTxList () {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
}
getFullTxList () {
return this.store.getState().transactions
}
// Returns the tx list
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
return txList.reduce((result, tx) => {
result[tx.id] = tx
return result
}, {})
}
getPendingTransactions (address) {
const opts = { status: 'submitted' }
if (address) opts.from = address
return this.getFilteredTxList(opts)
}
addTx (txMeta) {
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
})
this.once(`${txMeta.id}:rejected`, function (txId) {
this.removeAllListeners(`${txMeta.id}:signed`)
})
// initialize history
txMeta.history = []
// capture initial snapshot of txMeta for history
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history.push(snapshot)
const transactions = this.getFullTxList()
const txCount = this.getTxCount()
const txHistoryLimit = this.txHistoryLimit
// checks if the length of the tx history is
// longer then desired persistence limit
// and then if it is removes only confirmed
// or rejected tx's.
// not tx's that are pending or unapproved
if (txCount > txHistoryLimit - 1) {
const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
transactions.splice(index, 1)
}
transactions.push(txMeta)
this._saveTxList(transactions)
return txMeta
}
// gets tx by Id and returns it
getTx (txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0]
return txMeta
}
updateTx (txMeta, note) {
if (txMeta.txParams) {
Object.keys(txMeta.txParams).forEach((key) => {
let value = txMeta.txParams[key]
if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`)
if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed')
})
}
// create txMeta snapshot for history
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
// recover previous tx state obj
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
// generate history entry and add to history
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note)
txMeta.history.push(entry)
// commit txMeta to state
const txId = txMeta.id
const txList = this.getFullTxList()
const index = txList.findIndex(txData => txData.id === txId)
txList[index] = txMeta
this._saveTxList(txList)
}
// merges txParams obj onto txData.txParams
// use extend to ensure that all fields are filled
updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams)
this.updateTx(txMeta, `txStateManager#updateTxParams`)
}
/*
Takes an object of fields to search for eg:
let thingsToLookFor = {
to: '0x0..',
from: '0x0..',
status: 'signed',
err: undefined,
}
and returns a list of tx with all
options matching
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
| so you can also search txs that |
| dont have something as well by |
| setting the value as undefined |
************************************
this is for things like filtering a the tx list
for only tx's from 1 account
or for filltering for all txs from one account
and that have been 'confirmed'
*/
getFilteredTxList (opts, initialList) {
let filteredTxList = initialList
Object.keys(opts).forEach((key) => {
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
})
return filteredTxList
}
getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => {
if (txMeta.txParams[key]) {
return txMeta.txParams[key] === value
} else {
return txMeta[key] === value
}
})
}
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
// get::set status
// should return the status of the tx.
getTxStatus (txId) {
const txMeta = this.getTx(txId)
return txMeta.status
}
// should update the status of the tx to 'rejected'.
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
}
// should update the status of the tx to 'approved'.
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
}
// should update the status of the tx to 'signed'.
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
}
// should update the status of the tx to 'submitted'.
setTxStatusSubmitted (txId) {
this._setTxStatus(txId, 'submitted')
}
// should update the status of the tx to 'confirmed'.
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
}
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
stack: err.stack,
}
this.updateTx(txMeta)
this._setTxStatus(txId, 'failed')
}
//
// PRIVATE METHODS
//
// Should find the tx in the tx list and
// update it.
// should set the status in txData
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
_setTxStatus (txId, status) {
const txMeta = this.getTx(txId)
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status)
if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
this.emit('update:badge')
}
// Saves the new/updated txList.
// Function is intended only for internal use
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
}

View File

@ -14,7 +14,7 @@ const createOriginMiddleware = require('./lib/createOriginMiddleware')
const createLoggerMiddleware = require('./lib/createLoggerMiddleware') const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const createProviderMiddleware = require('./lib/createProviderMiddleware') const createProviderMiddleware = require('./lib/createProviderMiddleware')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const KeyringController = require('./keyring-controller') const KeyringController = require('eth-keyring-controller')
const NetworkController = require('./controllers/network') const NetworkController = require('./controllers/network')
const PreferencesController = require('./controllers/preferences') const PreferencesController = require('./controllers/preferences')
const CurrencyController = require('./controllers/currency') const CurrencyController = require('./controllers/currency')
@ -82,7 +82,7 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
this.blockTracker = this.provider this.blockTracker = this.provider._blockTracker
// eth data query tools // eth data query tools
this.ethQuery = new EthQuery(this.provider) this.ethQuery = new EthQuery(this.provider)
@ -100,6 +100,14 @@ module.exports = class MetamaskController extends EventEmitter {
encryptor: opts.encryptor || undefined, encryptor: opts.encryptor || undefined,
}) })
// If only one account exists, make sure it is selected.
this.keyringController.store.subscribe((state) => {
const addresses = Object.keys(state.walletNicknames || {})
if (addresses.length === 1) {
const address = addresses[0]
this.preferencesController.setSelectedAddress(address)
}
})
this.keyringController.on('newAccount', (address) => { this.keyringController.on('newAccount', (address) => {
this.preferencesController.setSelectedAddress(address) this.preferencesController.setSelectedAddress(address)
this.accountTracker.addAccount(address) this.accountTracker.addAccount(address)
@ -124,7 +132,6 @@ module.exports = class MetamaskController extends EventEmitter {
provider: this.provider, provider: this.provider,
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
ethQuery: this.ethQuery, ethQuery: this.ethQuery,
accountTracker: this.accountTracker,
}) })
this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts)) this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts))
@ -209,19 +216,18 @@ module.exports = class MetamaskController extends EventEmitter {
// //
initializeProvider () { initializeProvider () {
return this.networkController.initializeProvider({ const providerOpts = {
static: { static: {
eth_syncing: false, eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`, web3_clientVersion: `MetaMask/v${version}`,
}, },
// rpc data source
rpcUrl: this.networkController.getCurrentRpcAddress(),
originHttpHeaderKey: 'X-Metamask-Origin', originHttpHeaderKey: 'X-Metamask-Origin',
// account mgmt // account mgmt
getAccounts: (cb) => { getAccounts: (cb) => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const result = [] 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) result.push(selectedAddress)
@ -232,10 +238,11 @@ module.exports = class MetamaskController extends EventEmitter {
processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this),
// old style msg signing // old style msg signing
processMessage: this.newUnsignedMessage.bind(this), processMessage: this.newUnsignedMessage.bind(this),
// personal_sign msg signing
// new style msg signing
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
}) }
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
} }
initPublicConfigStore () { initPublicConfigStore () {
@ -304,13 +311,14 @@ module.exports = class MetamaskController extends EventEmitter {
const txController = this.txController const txController = this.txController
const noticeController = this.noticeController const noticeController = this.noticeController
const addressBookController = this.addressBookController const addressBookController = this.addressBookController
const networkController = this.networkController
return { return {
// etc // etc
getState: (cb) => cb(null, this.getState()), getState: (cb) => cb(null, this.getState()),
setProviderType: this.networkController.setProviderType.bind(this.networkController),
setCurrentCurrency: this.setCurrentCurrency.bind(this), setCurrentCurrency: this.setCurrentCurrency.bind(this),
markAccountsFound: this.markAccountsFound.bind(this), markAccountsFound: this.markAccountsFound.bind(this),
// coinbase // coinbase
buyEth: this.buyEth.bind(this), buyEth: this.buyEth.bind(this),
// shapeshift // shapeshift
@ -325,12 +333,14 @@ module.exports = class MetamaskController extends EventEmitter {
// vault management // vault management
submitPassword: this.submitPassword.bind(this), submitPassword: this.submitPassword.bind(this),
// network management
setProviderType: nodeify(networkController.setProviderType, networkController),
setCustomRpc: nodeify(this.setCustomRpc, this),
// PreferencesController // PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
addToken: nodeify(preferencesController.addToken, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setDefaultRpc: nodeify(this.setDefaultRpc, this),
setCustomRpc: nodeify(this.setCustomRpc, this),
// AddressController // AddressController
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
@ -681,19 +691,13 @@ module.exports = class MetamaskController extends EventEmitter {
createShapeShiftTx (depositAddress, depositType) { createShapeShiftTx (depositAddress, depositType) {
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
} }
// network
setDefaultRpc () { // network
this.networkController.setRpcTarget('http://localhost:8545')
return Promise.resolve('http://localhost:8545')
}
setCustomRpc (rpcTarget, rpcList) { async setCustomRpc (rpcTarget, rpcList) {
this.networkController.setRpcTarget(rpcTarget) this.networkController.setRpcTarget(rpcTarget)
await this.preferencesController.updateFrequentRpcList(rpcTarget)
return this.preferencesController.updateFrequentRpcList(rpcTarget) return rpcTarget
.then(() => {
return Promise.resolve(rpcTarget)
})
} }
} }

View File

@ -10,7 +10,7 @@ which we dont have access to at the time of this writing.
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ConfigManager = require('../../app/scripts/lib/config-manager') const ConfigManager = require('../../app/scripts/lib/config-manager')
const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator') const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator')
const KeyringController = require('../../app/scripts/lib/keyring-controller') const KeyringController = require('eth-keyring-controller')
const password = 'obviously not correct' const password = 'obviously not correct'

View File

@ -5,7 +5,6 @@ class ExtensionPlatform {
// //
// Public // Public
// //
reload () { reload () {
extension.runtime.reload() extension.runtime.reload()
} }

View File

@ -3,62 +3,58 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>MetaMask</title> <title>MetaMask</title>
</head> </head>
<body> <body>
<!-- app content -->
<div id="app-content" style="height: 100%"></div>
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script> <script src="./bundle.js" type="text/javascript" charset="utf-8"></script>
</body> <style>
html, body, #test-container, .super-dev-container {
<style> height: 100%;
html, body, #test-container, .super-dev-container { width: 100%;
height: 100%; position: relative;
width: 100%; background: white;
position: relative;
background: white;
}
#app-content {
background: #F7F7F7;
}
</style>
<script>
liveReloadCode(Date.now(), 300)
function liveReloadCode(lastUpdate, updateRate) {
setTimeout(iter, updateRate)
function iter() {
var xhr = new XMLHttpRequest()
xhr.open('GET', '/-/live-reload')
xhr.onreadystatechange = function() {
if(xhr.readyState !== 4) {
return
} }
#app-content {
background: #F7F7F7;
}
</style>
try { <script>
var change = JSON.parse(xhr.responseText).lastUpdate liveReloadCode(Date.now(), 300)
function liveReloadCode(lastUpdate, updateRate) {
setTimeout(iter, updateRate)
if(lastUpdate < change) { function iter() {
return reload() var xhr = new XMLHttpRequest()
xhr.open('GET', '/-/live-reload')
xhr.onreadystatechange = function() {
if(xhr.readyState !== 4) {
return
}
try {
var change = JSON.parse(xhr.responseText).lastUpdate
if(lastUpdate < change) {
return reload()
}
} catch(err) {
}
xhr =
xhr.onreadystatechange = null
setTimeout(iter, updateRate)
}
xhr.send(null)
} }
} catch(err) {
} }
xhr = function reload() {
xhr.onreadystatechange = null window.location.reload()
setTimeout(iter, updateRate) }
} </script>
xhr.send(null) </body>
}
}
function reload() {
window.location.reload()
}
</script>
</html> </html>

View File

@ -1,57 +1,26 @@
window.addEventListener('load', web3Detect) const EthQuery = require('ethjs-query')
window.addEventListener('load', loadProvider)
window.addEventListener('message', console.warn) window.addEventListener('message', console.warn)
function web3Detect() { async function loadProvider() {
if (global.web3) { const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' })
logToDom('web3 detected!') const ethQuery = new EthQuery(ethereumProvider)
startApp() const accounts = await ethQuery.accounts()
} else { logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
logToDom('no web3 detected!') setupButton(ethQuery)
}
} }
function startApp(){
console.log('app started')
var primaryAccount
console.log('getting main account...')
web3.eth.getAccounts((err, addresses) => {
if (err) console.error(err)
console.log('set address', addresses[0])
primaryAccount = addresses[0]
})
document.querySelector('.action-button-1').addEventListener('click', function(){
console.log('saw click')
console.log('sending tx')
primaryAccount
web3.eth.sendTransaction({
from: primaryAccount,
to: primaryAccount,
value: 0,
}, function(err, txHash){
if (err) throw err
console.log('sendTransaction result:', err || txHash)
})
})
document.querySelector('.action-button-2').addEventListener('click', function(){
console.log('saw click')
setTimeout(function(){
console.log('sending tx')
web3.eth.sendTransaction({
from: primaryAccount,
to: primaryAccount,
value: 0,
}, function(err, txHash){
if (err) throw err
console.log('sendTransaction result:', err || txHash)
})
})
})
}
function logToDom(message){ function logToDom(message){
document.body.appendChild(document.createTextNode(message)) document.getElementById('account').innerText = message
console.log(message) console.log(message)
} }
function setupButton (ethQuery) {
const button = document.getElementById('action-button-1')
button.addEventListener('click', async () => {
const accounts = await ethQuery.accounts()
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
})
}

View File

@ -3,13 +3,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>MetaMask ZeroClient Example</title>
<script src="http://localhost:9001/metamascara.js"></script> <script src="http://localhost:9001/metamascara.js"></script>
<title>MetaMask ZeroClient Example</title>
</head> </head>
<body> <body>
<button class="action-button-1">SYNC TX</button> <button id="action-button-1">GET ACCOUNT</button>
<button class="action-button-2">ASYNC TX</button> <div id="account"></div>
<script src="./app.js"></script> <script src="./app.js"></script>
</body> </body>
</html> </html>

View File

@ -19,8 +19,7 @@ const migrations = require('../../app/scripts/migrations/')
const firstTimeState = require('../../app/scripts/first-time-state') const firstTimeState = require('../../app/scripts/first-time-state')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
// const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const METAMASK_DEBUG = true
let popupIsOpen = false let popupIsOpen = false
let connectedClientCount = 0 let connectedClientCount = 0

View File

@ -1,19 +0,0 @@
const Iframe = require('iframe')
const createIframeStream = require('iframe-stream').IframeStream
module.exports = setupIframe
function setupIframe(opts) {
opts = opts || {}
var frame = Iframe({
src: opts.zeroClientProvider || 'https://zero.metamask.io/',
container: opts.container || document.head,
sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups'],
})
var iframe = frame.iframe
iframe.style.setProperty('display', 'none')
var iframeStream = createIframeStream(iframe)
return iframeStream
}

View File

@ -1,22 +0,0 @@
const setupIframe = require('./setup-iframe.js')
const MetamaskInpageProvider = require('../../../app/scripts/lib/inpage-provider.js')
module.exports = getProvider
function getProvider(opts){
if (global.web3) {
console.log('MetaMask ZeroClient - using environmental web3 provider')
return global.web3.currentProvider
}
console.log('MetaMask ZeroClient - injecting zero-client iframe!')
var iframeStream = setupIframe({
zeroClientProvider: opts.mascaraUrl,
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
container: document.body,
})
var inpageProvider = new MetamaskInpageProvider(iframeStream)
return inpageProvider
}

View File

@ -1,47 +1 @@
const Web3 = require('web3') global.metamask = require('metamascara')
const setupProvider = require('./lib/setup-provider.js')
const setupDappAutoReload = require('../../app/scripts/lib/auto-reload.js')
const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001'
console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN)
//
// setup web3
//
const provider = setupProvider({
mascaraUrl: MASCARA_ORIGIN + '/proxy/',
})
instrumentForUserInteractionTriggers(provider)
const web3 = new Web3(provider)
setupDappAutoReload(web3, provider.publicConfigStore)
//
// ui stuff
//
let shouldPop = false
window.addEventListener('click', maybeTriggerPopup)
//
// util
//
function maybeTriggerPopup(){
if (!shouldPop) return
shouldPop = false
window.open(MASCARA_ORIGIN, '', 'width=360 height=500')
console.log('opening window...')
}
function instrumentForUserInteractionTriggers(provider){
const _super = provider.sendAsync.bind(provider)
provider.sendAsync = function(payload, cb){
if (payload.method === 'eth_sendTransaction') {
console.log('saw send')
shouldPop = true
}
_super(payload, cb)
}
}

View File

@ -62,6 +62,7 @@ const controller = new MetamaskController({
showUnconfirmedMessage: noop, showUnconfirmedMessage: noop,
unlockAccountMessage: noop, unlockAccountMessage: noop,
showUnapprovedTx: noop, showUnapprovedTx: noop,
platform: {},
// initial state // initial state
initState: firstTimeState, initState: firstTimeState,
}) })

View File

@ -9,7 +9,7 @@
"ui": "npm run test:flat:build:states && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "ui": "npm run test:flat:build:states && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"watch": "mocha watch --recursive \"test/unit/**/*.js\"", "watch": "mocha watch --recursive \"test/unit/**/*.js\"",
"mascara": "node ./mascara/example/server", "mascara": "METAMASK_DEBUG=true node ./mascara/example/server",
"dist": "npm run dist:clear && npm install && gulp dist", "dist": "npm run dist:clear && npm install && gulp dist",
"dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect", "dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
"test": "npm run lint && npm run test:coverage && npm run test:integration", "test": "npm run lint && npm run test:coverage && npm run test:integration",
@ -53,10 +53,8 @@
"async": "^2.5.0", "async": "^2.5.0",
"await-semaphore": "^0.1.1", "await-semaphore": "^0.1.1",
"babel-runtime": "^6.23.0", "babel-runtime": "^6.23.0",
"bip39": "^2.2.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"bn.js": "^4.11.7", "bn.js": "^4.11.7",
"browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4", "browserify-derequire": "^0.9.4",
"client-sw-ready-event": "^3.3.0", "client-sw-ready-event": "^3.3.0",
"clone": "^2.1.1", "clone": "^2.1.1",
@ -69,9 +67,11 @@
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
"ensnare": "^1.0.0", "ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.2.0",
"eth-contract-metadata": "^1.1.4", "eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.1.1", "eth-hd-keyring": "^1.1.1",
"eth-json-rpc-filters": "^1.1.0", "eth-json-rpc-filters": "^1.2.1",
"eth-keyring-controller": "^2.0.0",
"eth-phishing-detect": "^1.1.4", "eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.2.2", "eth-sig-util": "^1.2.2",
@ -80,9 +80,10 @@
"ethereumjs-tx": "^1.3.0", "ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
"ethjs-contract": "^0.1.9",
"ethjs-ens": "^2.0.0", "ethjs-ens": "^2.0.0",
"ethjs-query": "^0.2.9", "ethjs-query": "^0.2.9",
"express": "^4.14.0", "express": "^4.15.5",
"extension-link-enabler": "^1.0.0", "extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0", "extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4", "fast-json-patch": "^2.0.4",
@ -90,6 +91,7 @@
"gulp": "github:gulpjs/gulp#4.0", "gulp": "github:gulpjs/gulp#4.0",
"gulp-eslint": "^4.0.0", "gulp-eslint": "^4.0.0",
"hat": "0.0.3", "hat": "0.0.3",
"human-standard-token-abi": "^1.0.2",
"idb-global": "^2.1.0", "idb-global": "^2.1.0",
"identicon.js": "^2.3.1", "identicon.js": "^2.3.1",
"iframe": "^1.0.0", "iframe": "^1.0.0",
@ -99,6 +101,7 @@
"json-rpc-engine": "^3.2.0", "json-rpc-engine": "^3.2.0",
"json-rpc-middleware-stream": "^1.0.1", "json-rpc-middleware-stream": "^1.0.1",
"loglevel": "^1.4.1", "loglevel": "^1.4.1",
"metamascara": "^1.3.1",
"metamask-logo": "^2.1.2", "metamask-logo": "^2.1.2",
"mississippi": "^1.2.0", "mississippi": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
@ -138,7 +141,7 @@
"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": "^13.2.9", "web3-provider-engine": "^13.3.1",
"web3-stream-provider": "^3.0.1", "web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },

View File

@ -24,7 +24,8 @@ describe('PendingTx', function () {
'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', 'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
'value': '0xde0b6b3a7640000', 'value': '0xde0b6b3a7640000',
gasPrice, gasPrice,
'gas': '0x7b0c'}, 'gas': '0x7b0c',
},
'gasLimitSpecified': false, 'gasLimitSpecified': false,
'estimatedGas': '0x5208', 'estimatedGas': '0x5208',
} }

View File

@ -15,11 +15,11 @@ describe('currency-controller', function () {
describe('currency conversions', function () { describe('currency conversions', function () {
describe('#setCurrentCurrency', function () { describe('#setCurrentCurrency', function () {
it('should return USD as default', function () { it('should return USD as default', function () {
assert.equal(currencyController.getCurrentCurrency(), 'USD') assert.equal(currencyController.getCurrentCurrency(), 'usd')
}) })
it('should be able to set to other currency', function () { it('should be able to set to other currency', function () {
assert.equal(currencyController.getCurrentCurrency(), 'USD') assert.equal(currencyController.getCurrentCurrency(), 'usd')
currencyController.setCurrentCurrency('JPY') currencyController.setCurrentCurrency('JPY')
var result = currencyController.getCurrentCurrency() var result = currencyController.getCurrentCurrency()
assert.equal(result, 'JPY') assert.equal(result, 'JPY')
@ -36,12 +36,12 @@ describe('currency-controller', function () {
describe('#updateConversionRate', function () { describe('#updateConversionRate', function () {
it('should retrieve an update for ETH to USD and set it in memory', function (done) { it('should retrieve an update for ETH to USD and set it in memory', function (done) {
this.timeout(15000) this.timeout(15000)
nock('https://api.cryptonator.com') nock('https://api.infura.io')
.get('/api/ticker/eth-USD') .get('/v1/ticker/ethusd')
.reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') .reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}')
assert.equal(currencyController.getConversionRate(), 0) assert.equal(currencyController.getConversionRate(), 0)
currencyController.setCurrentCurrency('USD') currencyController.setCurrentCurrency('usd')
currencyController.updateConversionRate() currencyController.updateConversionRate()
.then(function () { .then(function () {
var result = currencyController.getConversionRate() var result = currencyController.getConversionRate()
@ -57,14 +57,14 @@ describe('currency-controller', function () {
this.timeout(15000) this.timeout(15000)
assert.equal(currencyController.getConversionRate(), 0) assert.equal(currencyController.getConversionRate(), 0)
nock('https://api.cryptonator.com') nock('https://api.infura.io')
.get('/api/ticker/eth-JPY') .get('/v1/ticker/ethjpy')
.reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}') .reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}')
var promise = new Promise( var promise = new Promise(
function (resolve, reject) { function (resolve, reject) {
currencyController.setCurrentCurrency('JPY') currencyController.setCurrentCurrency('jpy')
currencyController.updateConversionRate().then(function () { currencyController.updateConversionRate().then(function () {
resolve() resolve()
}) })

View File

@ -1,164 +0,0 @@
const assert = require('assert')
const KeyringController = require('../../app/scripts/keyring-controller')
const configManagerGen = require('../lib/mock-config-manager')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const mockEncryptor = require('../lib/mock-encryptor')
const sinon = require('sinon')
describe('KeyringController', function () {
let keyringController
const password = 'password123'
const seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway'
const addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()]
const accounts = []
// let originalKeystore
beforeEach(function (done) {
this.sinon = sinon.sandbox.create()
window.localStorage = {} // Hacking localStorage support into JSDom
keyringController = new KeyringController({
configManager: configManagerGen(),
txManager: {
getTxList: () => [],
getUnapprovedTxList: () => [],
},
accountTracker: {
addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
},
encryptor: mockEncryptor,
})
keyringController.createNewVaultAndKeychain(password)
.then(function (newState) {
newState
done()
})
.catch((err) => {
done(err)
})
})
afterEach(function () {
// Cleanup mocks
this.sinon.restore()
})
describe('#createNewVaultAndKeychain', function () {
this.timeout(10000)
it('should set a vault on the configManager', function (done) {
keyringController.store.updateState({ vault: null })
assert(!keyringController.store.getState().vault, 'no previous vault')
keyringController.createNewVaultAndKeychain(password)
.then(() => {
const vault = keyringController.store.getState().vault
assert(vault, 'vault created')
done()
})
.catch((reason) => {
done(reason)
})
})
})
describe('#restoreKeyring', function () {
it(`should pass a keyring's serialized data back to the correct type.`, function (done) {
const mockSerialized = {
type: 'HD Key Tree',
data: {
mnemonic: seedWords,
numberOfAccounts: 1,
},
}
const mock = this.sinon.mock(keyringController)
mock.expects('getBalanceAndNickname')
.exactly(1)
keyringController.restoreKeyring(mockSerialized)
.then((keyring) => {
assert.equal(keyring.wallets.length, 1, 'one wallet restored')
return keyring.getAccounts()
})
.then((accounts) => {
assert.equal(accounts[0], addresses[0])
mock.verify()
done()
})
.catch((reason) => {
done(reason)
})
})
})
describe('#createNickname', function () {
it('should add the address to the identities hash', function () {
const fakeAddress = '0x12345678'
keyringController.createNickname(fakeAddress)
const identities = keyringController.memStore.getState().identities
const identity = identities[fakeAddress]
assert.equal(identity.address, fakeAddress)
})
})
describe('#saveAccountLabel', function () {
it('sets the nickname', function (done) {
const account = addresses[0]
var nick = 'Test nickname'
const identities = keyringController.memStore.getState().identities
identities[ethUtil.addHexPrefix(account)] = {}
keyringController.memStore.updateState({ identities })
keyringController.saveAccountLabel(account, nick)
.then((label) => {
try {
assert.equal(label, nick)
const persisted = keyringController.store.getState().walletNicknames[account]
assert.equal(persisted, nick)
done()
} catch (err) {
done()
}
})
.catch((reason) => {
done(reason)
})
})
})
describe('#getAccounts', function () {
it('returns the result of getAccounts for each keyring', function (done) {
keyringController.keyrings = [
{ getAccounts () { return Promise.resolve([1, 2, 3]) } },
{ getAccounts () { return Promise.resolve([4, 5, 6]) } },
]
keyringController.getAccounts()
.then((result) => {
assert.deepEqual(result, [1, 2, 3, 4, 5, 6])
done()
})
})
})
describe('#addGasBuffer', function () {
it('adds 100k gas buffer to estimates', function () {
const gas = '0x04ee59' // Actual estimated gas example
const tooBigOutput = '0x80674f9' // Actual bad output
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
const correctBuffer = new BN('100000', 10)
const correct = bnGas.add(correctBuffer)
// const tooBig = new BN(tooBigOutput, 16)
const result = keyringController.addGasBuffer(gas)
const bnResult = new BN(ethUtil.stripHexPrefix(result), 16)
assert.equal(result.indexOf('0x'), 0, 'included hex prefix')
assert(bnResult.gt(bnGas), 'Estimate increased in value.')
assert.equal(bnResult.sub(bnGas).toString(10), '100000', 'added 100k gas')
assert.equal(result, '0x' + correct.toString(16), 'Added the right amount')
assert.notEqual(result, tooBigOutput, 'not that bad estimate')
})
})
})

View File

@ -10,6 +10,7 @@ describe('MetaMaskController', function () {
showUnconfirmedMessage: noop, showUnconfirmedMessage: noop,
unlockAccountMessage: noop, unlockAccountMessage: noop,
showUnapprovedTx: noop, showUnapprovedTx: noop,
platform: {},
// initial state // initial state
initState: clone(firstTimeState), initState: clone(firstTimeState),
}) })

View File

@ -20,9 +20,9 @@ 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(networkControllerProviderInit, dummyProviderConstructor) networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
const provider = networkController.provider const proxy = networkController._proxy
networkController._provider = {test: true} proxy.setTarget({ test: true, on: () => {} })
assert.ok(provider.test) assert.ok(proxy.test)
}) })
}) })
describe('#getNetworkState', function () { describe('#getNetworkState', function () {
@ -71,6 +71,7 @@ function dummyProviderConstructor() {
// provider // provider
sendAsync: noop, sendAsync: noop,
// block tracker // block tracker
_blockTracker: {},
start: noop, start: noop,
stop: noop, stop: noop,
on: noop, on: noop,

View File

@ -40,14 +40,12 @@ describe('PendingTransactionTracker', function () {
pendingTxTracker = new PendingTransactionTracker({ pendingTxTracker = new PendingTransactionTracker({
provider, provider,
getBalance: () => {},
nonceTracker: { nonceTracker: {
getGlobalLock: async () => { getGlobalLock: async () => {
return { releaseLock: () => {} } return { releaseLock: () => {} }
} }
}, },
getPendingTransactions: () => {return []}, getPendingTransactions: () => {return []},
sufficientBalance: () => {},
publishTransaction: () => {}, publishTransaction: () => {},
}) })
}) })
@ -62,7 +60,7 @@ describe('PendingTransactionTracker', function () {
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
const block = Proxy.revocable({}, {}).revoke() const block = Proxy.revocable({}, {}).revoke()
pendingTxTracker.getPendingTransactions = () => [txMetaNoHash] pendingTxTracker.getPendingTransactions = () => [txMetaNoHash]
pendingTxTracker.once('txFailed', (txId, err) => { pendingTxTracker.once('tx:failed', (txId, err) => {
assert(txId, txMetaNoHash.id, 'should pass txId') assert(txId, txMetaNoHash.id, 'should pass txId')
done() done()
}) })
@ -71,11 +69,11 @@ describe('PendingTransactionTracker', function () {
it('should emit \'txConfirmed\' if the tx is in the block', function (done) { it('should emit \'txConfirmed\' if the tx is in the block', function (done) {
const block = { transactions: [txMeta]} const block = { transactions: [txMeta]}
pendingTxTracker.getPendingTransactions = () => [txMeta] pendingTxTracker.getPendingTransactions = () => [txMeta]
pendingTxTracker.once('txConfirmed', (txId) => { pendingTxTracker.once('tx:confirmed', (txId) => {
assert(txId, txMeta.id, 'should pass txId') assert(txId, txMeta.id, 'should pass txId')
done() done()
}) })
pendingTxTracker.once('txFailed', (_, err) => { done(err) }) pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
pendingTxTracker.checkForTxInBlock(block) pendingTxTracker.checkForTxInBlock(block)
}) })
}) })
@ -84,14 +82,14 @@ describe('PendingTransactionTracker', function () {
let newBlock, oldBlock let newBlock, oldBlock
newBlock = { number: '0x01' } newBlock = { number: '0x01' }
pendingTxTracker._checkPendingTxs = done pendingTxTracker._checkPendingTxs = done
pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
}) })
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) {
let newBlock, oldBlock let newBlock, oldBlock
oldBlock = { number: '0x01' } oldBlock = { number: '0x01' }
newBlock = { number: '0x03' } newBlock = { number: '0x03' }
pendingTxTracker._checkPendingTxs = done pendingTxTracker._checkPendingTxs = done
pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
}) })
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) {
let newBlock, oldBlock let newBlock, oldBlock
@ -101,14 +99,14 @@ describe('PendingTransactionTracker', function () {
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less')
done(err) done(err)
} }
pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
done() done()
}) })
}) })
describe('#_checkPendingTx', function () { describe('#_checkPendingTx', function () {
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
pendingTxTracker.once('txFailed', (txId, err) => { pendingTxTracker.once('tx:failed', (txId, err) => {
assert(txId, txMetaNoHash.id, 'should pass txId') assert(txId, txMetaNoHash.id, 'should pass txId')
done() done()
}) })
@ -122,11 +120,11 @@ describe('PendingTransactionTracker', function () {
it('should emit \'txConfirmed\'', function (done) { it('should emit \'txConfirmed\'', function (done) {
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'} providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'}
pendingTxTracker.once('txConfirmed', (txId) => { pendingTxTracker.once('tx:confirmed', (txId) => {
assert(txId, txMeta.id, 'should pass txId') assert(txId, txMeta.id, 'should pass txId')
done() done()
}) })
pendingTxTracker.once('txFailed', (_, err) => { done(err) }) pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
pendingTxTracker._checkPendingTx(txMeta) pendingTxTracker._checkPendingTx(txMeta)
}) })
}) })
@ -188,7 +186,7 @@ describe('PendingTransactionTracker', function () {
] ]
const enoughForAllErrors = txList.concat(txList) const enoughForAllErrors = txList.concat(txList)
pendingTxTracker.on('txFailed', (_, err) => done(err)) pendingTxTracker.on('tx:failed', (_, err) => done(err))
pendingTxTracker.getPendingTransactions = () => enoughForAllErrors pendingTxTracker.getPendingTransactions = () => enoughForAllErrors
pendingTxTracker._resubmitTx = async (tx) => { pendingTxTracker._resubmitTx = async (tx) => {
@ -202,7 +200,7 @@ describe('PendingTransactionTracker', function () {
pendingTxTracker.resubmitPendingTxs() pendingTxTracker.resubmitPendingTxs()
}) })
it('should emit \'txFailed\' if it encountered a real error', function (done) { it('should emit \'txFailed\' if it encountered a real error', function (done) {
pendingTxTracker.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) pendingTxTracker.once('tx:failed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err))
pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker.getPendingTransactions = () => txList
pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') }
@ -213,30 +211,7 @@ describe('PendingTransactionTracker', function () {
pendingTxTracker.resubmitPendingTxs() pendingTxTracker.resubmitPendingTxs()
}) })
}) })
describe('#_resubmitTx with a too-low balance', function () { describe('#_resubmitTx', function () {
it('should return before publishing the transaction because to low of balance', function (done) {
const lowBalance = '0x0'
pendingTxTracker.getBalance = (address) => {
assert.equal(address, txMeta.txParams.from, 'Should pass the address')
return lowBalance
}
pendingTxTracker.publishTransaction = async (rawTx) => {
done(new Error('tried to publish transaction'))
}
// Stubbing out current account state:
// Adding the fake tx:
pendingTxTracker.once('txFailed', (txId, err) => {
assert(err, 'Should have a error')
done()
})
pendingTxTracker._resubmitTx(txMeta)
.catch((err) => {
assert.ifError(err, 'should not throw an error')
done(err)
})
})
it('should publishing the transaction', function (done) { it('should publishing the transaction', function (done) {
const enoughBalance = '0x100000' const enoughBalance = '0x100000'
pendingTxTracker.getBalance = (address) => { pendingTxTracker.getBalance = (address) => {

View File

@ -2,21 +2,19 @@ const assert = require('assert')
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')
const clone = require('clone')
const sinon = require('sinon') const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions') const TransactionController = require('../../app/scripts/controllers/transactions')
const TxProvideUtils = require('../../app/scripts/lib/tx-utils') const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') const { createStubedProvider } = require('../stub/provider')
const noop = () => true const noop = () => true
const currentNetworkId = 42 const currentNetworkId = 42
const otherNetworkId = 36 const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
const { createStubedProvider } = require('../stub/provider')
describe('Transaction Controller', function () { describe('Transaction Controller', function () {
let txController, engine, provider, providerResultStub let txController, provider, providerResultStub
beforeEach(function () { beforeEach(function () {
providerResultStub = {} providerResultStub = {}
@ -27,34 +25,96 @@ describe('Transaction Controller', function () {
networkStore: new ObservableStore(currentNetworkId), networkStore: new ObservableStore(currentNetworkId),
txHistoryLimit: 10, txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
accountTracker: { getState: noop },
signTransaction: (ethTx) => new Promise((resolve) => { signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey) ethTx.sign(privKey)
resolve() resolve()
}), }),
}) })
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
txController.txProviderUtils = new TxProvideUtils(txController.provider) txController.txProviderUtils = new TxGasUtils(txController.provider)
}) })
describe('#getState', function () {
it('should return a state object with the right keys and datat types', function () {
const exposedState = txController.getState()
assert('unapprovedTxs' in exposedState, 'state should have the key unapprovedTxs')
assert('selectedAddressTxList' in exposedState, 'state should have the key selectedAddressTxList')
assert(typeof exposedState.unapprovedTxs === 'object', 'should be an object')
assert(Array.isArray(exposedState.selectedAddressTxList), 'should be an array')
})
})
describe('#getUnapprovedTxCount', function () {
it('should return the number of unapproved txs', function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 2, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
])
const unapprovedTxCount = txController.getUnapprovedTxCount()
assert.equal(unapprovedTxCount, 3, 'should be 3')
})
})
describe('#getPendingTxCount', function () {
it('should return the number of pending txs', function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
])
const pendingTxCount = txController.getPendingTxCount()
assert.equal(pendingTxCount, 3, 'should be 3')
})
})
describe('#getConfirmedTransactions', function () {
let address
beforeEach(function () {
address = '0xc684832530fcbddae4b4230a47e991ddcec2831d'
const txParams = {
'from': address,
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}
txController.txStateManager._saveTxList([
{id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
{id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
{id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
{id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams},
{id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams},
{id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams},
{id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams},
{id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams},
{id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams},
])
})
it('should return the number of confirmed txs', function () {
assert.equal(txController.nonceTracker.getConfirmedTransactions(address).length, 3)
})
})
describe('#newUnapprovedTransaction', function () { describe('#newUnapprovedTransaction', function () {
let stub, txMeta, txParams let stub, txMeta, txParams
beforeEach(function () { beforeEach(function () {
txParams = { txParams = {
'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}, }
txMeta = { txMeta = {
status: 'unapproved', status: 'unapproved',
id: 1, id: 1,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams, txParams,
history: [],
} }
txController.addTx(txMeta) txController.txStateManager._saveTxList([txMeta])
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta)) stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txController.txStateManager.addTx(txMeta)))
}) })
afterEach(function () { afterEach(function () {
txController.txStateManager._saveTxList([])
stub.restore() stub.restore()
}) })
@ -72,7 +132,7 @@ describe('Transaction Controller', function () {
txController.once('newUnaprovedTx', (txMetaFromEmit) => { txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => { setTimeout(() => {
txController.setTxHash(txMetaFromEmit.id, '0x0') txController.setTxHash(txMetaFromEmit.id, '0x0')
txController.setTxStatusSubmitted(txMetaFromEmit.id) txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id)
}, 10) }, 10)
}) })
@ -87,7 +147,7 @@ describe('Transaction Controller', function () {
it('should reject when finished and status is rejected', function (done) { it('should reject when finished and status is rejected', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => { txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => { setTimeout(() => {
txController.setTxStatusRejected(txMetaFromEmit.id) txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id)
}, 10) }, 10)
}) })
@ -110,7 +170,7 @@ describe('Transaction Controller', function () {
assert(('txParams' in txMeta), 'should have a txParams') assert(('txParams' in txMeta), 'should have a txParams')
assert(('history' in txMeta), 'should have a history') assert(('history' in txMeta), 'should have a history')
const memTxMeta = txController.getTx(txMeta.id) const memTxMeta = txController.txStateManager.getTx(txMeta.id)
assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`) assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`)
addTxDefaultsStub.restore() addTxDefaultsStub.restore()
done() done()
@ -120,10 +180,10 @@ describe('Transaction Controller', function () {
describe('#addTxDefaults', function () { describe('#addTxDefaults', function () {
it('should add the tx defaults if their are none', function (done) { it('should add the tx defaults if their are none', function (done) {
let txMeta = { const txMeta = {
'txParams': { 'txParams': {
'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}, },
} }
providerResultStub.eth_gasPrice = '4a817c800' providerResultStub.eth_gasPrice = '4a817c800'
@ -131,7 +191,7 @@ describe('Transaction Controller', function () {
providerResultStub.eth_estimateGas = '5209' providerResultStub.eth_estimateGas = '5209'
txController.addTxDefaults(txMeta) txController.addTxDefaults(txMeta)
.then((txMetaWithDefaults) => { .then((txMetaWithDefaults) => {
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() done()
@ -163,214 +223,31 @@ describe('Transaction Controller', function () {
}) })
}) })
describe('#getTxList', function () {
it('when new should return empty array', function () {
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#addTx', function () { describe('#addTx', function () {
it('adds a tx returned in getTxList', function () { it('should emit updates', function (done) {
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('does not override txs from other networks', function () {
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
txController.addTx(tx, noop)
txController.addTx(tx2, noop)
var result = txController.getFullTxList()
var result2 = txController.getTxList()
assert.equal(result.length, 2, 'txs were deleted')
assert.equal(result2.length, 1, 'incorrect number of txs on network.')
})
it('cuts off early txs beyond a limit', function () {
const limit = txController.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
}
var result = txController.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () {
const limit = txController.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
}
var result = txController.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () {
var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(unconfirmedTx, noop)
const limit = txController.txHistoryLimit
for (let i = 1; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
}
var result = txController.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 0, 'first tx should still be there')
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
assert.equal(result[1].id, 2, 'early txs truncted')
})
})
describe('#setTxStatusSigned', function () {
it('sets the tx status to signed', function () {
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
txController.setTxStatusSigned(1)
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'signed')
})
it('should emit a signed event to signal the exciton of callback', (done) => {
this.timeout(10000)
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
const noop = function () {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txController.addTx(tx)
txController.on('1:signed', noop)
txController.setTxStatusSigned(1)
})
})
describe('#setTxStatusRejected', function () {
it('sets the tx status to rejected', function () {
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx)
txController.setTxStatusRejected(1)
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'rejected')
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
this.timeout(10000)
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx)
const noop = function (err, txId) {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txController.on('1:rejected', noop)
txController.setTxStatusRejected(1)
})
})
describe('#updateTx', function () {
it('replaces the tx with the same id', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const tx1 = txController.getTx('1')
tx1.status = 'blah'
tx1.hash = 'foo'
txController.updateTx(tx1)
const savedResult = txController.getTx('1')
assert.equal(savedResult.hash, 'foo')
})
it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01'
const desiredGasPrice = '0x02'
const txMeta = { const txMeta = {
id: '1', id: '1',
status: 'unapproved', status: 'unapproved',
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: { txParams: {},
gasPrice: originalGasPrice,
},
} }
const eventNames = ['update:badge', '1:unapproved']
const listeners = []
eventNames.forEach((eventName) => {
listeners.push(new Promise((resolve) => {
txController.once(eventName, (arg) => {
resolve(arg)
})
}))
})
Promise.all(listeners)
.then((returnValues) => {
assert.deepEqual(returnValues.pop(), txMeta, 'last event 1:unapproved should return txMeta')
done()
})
.catch(done)
txController.addTx(txMeta) txController.addTx(txMeta)
const updatedTx = txController.getTx('1')
// verify tx was initialized correctly
assert.equal(updatedTx.history.length, 1, 'one history item (initial)')
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state')
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
// modify value and updateTx
updatedTx.txParams.gasPrice = desiredGasPrice
txController.updateTx(updatedTx)
// check updated value
const result = txController.getTx('1')
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
// validate history was updated
assert.equal(result.history.length, 2, 'two history items (initial + diff)')
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)')
})
})
describe('#getUnapprovedTxList', function () {
it('returns unapproved txs in a hash', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const result = txController.getUnapprovedTxList()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unapproved')
assert.equal(result['2'], undefined)
})
})
describe('#getTx', function () {
it('returns a tx with the requested id', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
assert.equal(txController.getTx('1').status, 'unapproved')
assert.equal(txController.getTx('2').status, 'confirmed')
})
})
describe('#getFilteredTxList', function () {
it('returns a tx with the requested data', function () {
const txMetas = [
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
]
txMetas.forEach((txMeta) => txController.addTx(txMeta, noop))
let filterParams
filterParams = { status: 'unapproved', from: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'unapproved', to: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed', from: '0xbb' }
assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed' }
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { from: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { to: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
}) })
}) })
@ -404,11 +281,11 @@ describe('Transaction Controller', function () {
const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
txController.setTxHash('1', originalValue) txController.setTxHash('1', originalValue)
txController.setTxStatusSubmitted('1') txController.txStateManager.setTxStatusSubmitted('1')
}) })
txController.approveTransaction(txMeta.id).then(() => { txController.approveTransaction(txMeta.id).then(() => {
const result = txController.getTx(txMeta.id) const result = txController.txStateManager.getTx(txMeta.id)
const params = result.txParams const params = result.txParams
assert.equal(params.gas, originalValue, 'gas unmodified') assert.equal(params.gas, originalValue, 'gas unmodified')
@ -431,4 +308,96 @@ describe('Transaction Controller', function () {
}).catch(done) }).catch(done)
}) })
}) })
describe('#updateAndApproveTransaction', function () {
let txMeta
beforeEach(function () {
txMeta = {
id: 1,
status: 'unapproved',
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
metamaskNetworkId: currentNetworkId,
}
})
it('should update and approve transactions', function () {
txController.txStateManager.addTx(txMeta)
txController.updateAndApproveTransaction(txMeta)
const tx = txController.txStateManager.getTx(1)
assert.equal(tx.status, 'approved')
})
})
describe('#getChainId', function () {
it('returns 0 when the chainId is NaN', function () {
txController.networkStore = new ObservableStore(NaN)
assert.equal(txController.getChainId(), 0)
})
})
describe('#cancelTransaction', function () {
beforeEach(function () {
txController.txStateManager._saveTxList([
{ id: 0, status: 'unapproved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 1, status: 'rejected', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 2, status: 'approved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 3, status: 'signed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 4, status: 'submitted', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 5, status: 'confirmed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 6, status: 'failed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
])
})
it('should set the transaction to rejected from unapproved', async function () {
await txController.cancelTransaction(0)
assert.equal(txController.txStateManager.getTx(0).status, 'rejected')
})
})
describe('#publishTransaction', function () {
let hash, txMeta
beforeEach(function () {
hash = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8'
txMeta = {
id: 1,
status: 'unapproved',
txParams: {},
metamaskNetworkId: currentNetworkId,
}
providerResultStub.eth_sendRawTransaction = hash
})
it('should publish a tx, updates the rawTx when provided a one', async function () {
txController.txStateManager.addTx(txMeta)
await txController.publishTransaction(txMeta.id)
const publishedTx = txController.txStateManager.getTx(1)
assert.equal(publishedTx.hash, hash)
assert.equal(publishedTx.status, 'submitted')
})
})
describe('#getPendingTransactions', function () {
beforeEach(function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 2, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 6, status: 'confimed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} },
])
})
it('should show only submitted transactions as pending transasction', function () {
assert(txController.pendingTxTracker.getPendingTransactions().length, 1)
assert(txController.pendingTxTracker.getPendingTransactions()[0].status, 'submitted')
})
})
}) })

View File

@ -20,4 +20,27 @@ describe('tx-state-history-helper', function () {
}) })
}) })
}) })
it('replaying history does not mutate the original obj', function () {
const initialState = { test: true, message: 'hello', value: 1 }
const diff1 = [{
"op": "replace",
"path": "/message",
"value": "haay",
}]
const diff2 = [{
"op": "replace",
"path": "/value",
"value": 2,
}]
const history = [initialState, diff1, diff2]
const beforeStateSnapshot = JSON.stringify(initialState)
const latestState = txStateHistoryHelper.replayHistory(history)
const afterStateSnapshot = JSON.stringify(initialState)
assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state')
assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run')
})
}) })

View File

@ -0,0 +1,241 @@
const assert = require('assert')
const clone = require('clone')
const ObservableStore = require('obs-store')
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManger', function () {
let txStateManager
const currentNetworkId = 42
const otherNetworkId = 2
beforeEach(function () {
txStateManager = new TxStateManager({
initState: {
transactions: [],
},
txHistoryLimit: 10,
getNetwork: () => currentNetworkId
})
})
describe('#setTxStatusSigned', function () {
it('sets the tx status to signed', function () {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
txStateManager.setTxStatusSigned(1)
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'signed')
})
it('should emit a signed event to signal the exciton of callback', (done) => {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
const noop = function () {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txStateManager.addTx(tx)
txStateManager.on('1:signed', noop)
txStateManager.setTxStatusSigned(1)
})
})
describe('#setTxStatusRejected', function () {
it('sets the tx status to rejected', function () {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
txStateManager.setTxStatusRejected(1)
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'rejected')
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
const noop = function (err, txId) {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txStateManager.on('1:rejected', noop)
txStateManager.setTxStatusRejected(1)
})
})
describe('#getFullTxList', function () {
it('when new should return empty array', function () {
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#getTxList', function () {
it('when new should return empty array', function () {
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#addTx', function () {
it('adds a tx returned in getTxList', function () {
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('does not override txs from other networks', function () {
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
let tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
txStateManager.addTx(tx2, noop)
let result = txStateManager.getFullTxList()
let result2 = txStateManager.getTxList()
assert.equal(result.length, 2, 'txs were deleted')
assert.equal(result2.length, 1, 'incorrect number of txs on network.')
})
it('cuts off early txs beyond a limit', function () {
const limit = txStateManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
}
let result = txStateManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () {
const limit = txStateManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
}
let result = txStateManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () {
let unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(unconfirmedTx, noop)
const limit = txStateManager.txHistoryLimit
for (let i = 1; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
}
let result = txStateManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 0, 'first tx should still be there')
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
assert.equal(result[1].id, 2, 'early txs truncted')
})
})
describe('#updateTx', function () {
it('replaces the tx with the same id', function () {
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const txMeta = txStateManager.getTx('1')
txMeta.hash = 'foo'
txStateManager.updateTx(txMeta)
let result = txStateManager.getTx('1')
assert.equal(result.hash, 'foo')
})
it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01'
const desiredGasPrice = '0x02'
const txMeta = {
id: '1',
status: 'unapproved',
metamaskNetworkId: currentNetworkId,
txParams: {
gasPrice: originalGasPrice,
},
}
const updatedMeta = clone(txMeta)
txStateManager.addTx(txMeta)
const updatedTx = txStateManager.getTx('1')
// verify tx was initialized correctly
assert.equal(updatedTx.history.length, 1, 'one history item (initial)')
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state')
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
// modify value and updateTx
updatedTx.txParams.gasPrice = desiredGasPrice
txStateManager.updateTx(updatedTx)
// check updated value
const result = txStateManager.getTx('1')
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
// validate history was updated
assert.equal(result.history.length, 2, 'two history items (initial + diff)')
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)')
})
})
describe('#getUnapprovedTxList', function () {
it('returns unapproved txs in a hash', function () {
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const result = txStateManager.getUnapprovedTxList()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unapproved')
assert.equal(result['2'], undefined)
})
})
describe('#getTx', function () {
it('returns a tx with the requested id', function () {
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
assert.equal(txStateManager.getTx('1').status, 'unapproved')
assert.equal(txStateManager.getTx('2').status, 'confirmed')
})
})
describe('#getFilteredTxList', function () {
it('returns a tx with the requested data', function () {
const txMetas = [
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
]
txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop))
let filterParams
filterParams = { status: 'unapproved', from: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'unapproved', to: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed', from: '0xbb' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { from: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { to: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
})
})
})

View File

@ -1,8 +1,10 @@
const assert = require('assert') const assert = require('assert')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js') const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/lib/tx-utils') const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
describe('txUtils', function () { describe('txUtils', function () {
@ -28,7 +30,7 @@ describe('txUtils', function () {
nonce: '0x3', nonce: '0x3',
chainId: 42, chainId: 42,
} }
const ethTx = txUtils.buildEthTxFromParams(txParams) const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params') assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
}) })
}) })

View File

@ -61,30 +61,37 @@ const actions = {
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
const container = document.querySelector('#test-container')
// parse opts // parse opts
var store = configureStore(states[selectedView]) var store = configureStore(states[selectedView])
// start app // start app
render( startApp()
h('.super-dev-container', [
h(Selector, { actions, selectedKey: selectedView, states, store }), function startApp(){
const body = document.body
const container = document.createElement('div')
container.id = 'test-container'
body.appendChild(container)
h('#app-content', { render(
style: { h('.super-dev-container', [
height: '500px',
width: '360px',
boxShadow: 'grey 0px 2px 9px',
margin: '20px',
},
}, [
h(Root, {
store: store,
}),
]),
] h(Selector, { actions, selectedKey: selectedView, states, store }),
), container)
h('#app-content', {
style: {
height: '500px',
width: '360px',
boxShadow: 'grey 0px 2px 9px',
margin: '20px',
},
}, [
h(Root, {
store: store,
}),
]),
]
), container)
}

View File

@ -119,14 +119,11 @@ var actions = {
SET_RPC_TARGET: 'SET_RPC_TARGET', SET_RPC_TARGET: 'SET_RPC_TARGET',
SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET',
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER',
useEtherscanProvider: useEtherscanProvider,
showConfigPage, showConfigPage,
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
showAddTokenPage, showAddTokenPage,
addToken, addToken,
setRpcTarget: setRpcTarget, setRpcTarget: setRpcTarget,
setDefaultRpcTarget: setDefaultRpcTarget,
setProviderType: setProviderType, setProviderType: setProviderType,
// loading overlay // loading overlay
SHOW_LOADING: 'SHOW_LOADING_INDICATION', SHOW_LOADING: 'SHOW_LOADING_INDICATION',
@ -706,16 +703,19 @@ function markAccountsFound () {
// config // config
// //
// default rpc target refers to localhost:8545 in this instance. function setProviderType (type) {
function setDefaultRpcTarget () {
log.debug(`background.setDefaultRpcTarget`)
return (dispatch) => { return (dispatch) => {
background.setDefaultRpc((err, result) => { log.debug(`background.setProviderType`)
background.setProviderType(type, (err, result) => {
if (err) { if (err) {
log.error(err) log.error(err)
return dispatch(self.displayWarning('Had a problem changing networks.')) return dispatch(self.displayWarning('Had a problem changing networks!'))
} }
}) })
return {
type: actions.SET_PROVIDER_TYPE,
value: type,
}
} }
} }
@ -744,23 +744,6 @@ function addToAddressBook (recipient, nickname) {
} }
} }
function setProviderType (type) {
log.debug(`background.setProviderType`)
background.setProviderType(type)
return {
type: actions.SET_PROVIDER_TYPE,
value: type,
}
}
function useEtherscanProvider () {
log.debug(`background.useEtherscanProvider`)
background.useEtherscanProvider()
return {
type: actions.USE_ETHERSCAN_PROVIDER,
}
}
function showLoadingIndication (message) { function showLoadingIndication (message) {
return { return {
type: actions.SHOW_LOADING, type: actions.SHOW_LOADING,

View File

@ -319,7 +319,7 @@ App.prototype.renderNetworkDropdown = function () {
{ {
key: 'default', key: 'default',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setDefaultRpcTarget()), onClick: () => props.dispatch(actions.setProviderType('localhost')),
style: { style: {
fontSize: '18px', fontSize: '18px',
}, },

View File

@ -13,6 +13,7 @@ function FiatValue () {
FiatValue.prototype.render = function () { FiatValue.prototype.render = function () {
const props = this.props const props = this.props
const { conversionRate, currentCurrency } = props const { conversionRate, currentCurrency } = props
const renderedCurrency = currentCurrency || ''
const value = formatBalance(props.value, 6) const value = formatBalance(props.value, 6)
@ -28,7 +29,7 @@ FiatValue.prototype.render = function () {
fiatTooltipNumber = 'Unknown' fiatTooltipNumber = 'Unknown'
} }
return fiatDisplay(fiatDisplayNumber, currentCurrency) return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase())
} }
function fiatDisplay (fiatDisplayNumber, fiatSuffix) { function fiatDisplay (fiatDisplayNumber, fiatSuffix) {

View File

@ -3,7 +3,9 @@ const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const currencies = require('./conversion.json').rows const infuraCurrencies = require('./infura-conversion.json').objects.sort((a, b) => {
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
})
const validUrl = require('valid-url') const validUrl = require('valid-url')
const exportAsFile = require('./util').exportAsFile const exportAsFile = require('./util').exportAsFile
@ -167,8 +169,8 @@ function currentConversionInformation (metamaskState, state) {
state.dispatch(actions.setCurrentCurrency(newCurrency)) state.dispatch(actions.setCurrentCurrency(newCurrency))
}, },
defaultValue: currentCurrency, defaultValue: currentCurrency,
}, currencies.map((currency) => { }, infuraCurrencies.map((currency) => {
return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`)
}) })
), ),
]) ])

View File

@ -1,207 +0,0 @@
{
"rows": [
{
"code": "REP",
"name": "Augur",
"statuses": [
"primary"
]
},
{
"code": "BCN",
"name": "Bytecoin",
"statuses": [
"primary"
]
},
{
"code": "BTC",
"name": "Bitcoin",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "BTS",
"name": "BitShares",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "BLK",
"name": "Blackcoin",
"statuses": [
"primary"
]
},
{
"code": "GBP",
"name": "British Pound Sterling",
"statuses": [
"secondary"
]
},
{
"code": "CAD",
"name": "Canadian Dollar",
"statuses": [
"secondary"
]
},
{
"code": "CNY",
"name": "Chinese Yuan",
"statuses": [
"secondary"
]
},
{
"code": "DSH",
"name": "Dashcoin",
"statuses": [
"primary"
]
},
{
"code": "DOGE",
"name": "Dogecoin",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "ETC",
"name": "Ethereum Classic",
"statuses": [
"primary"
]
},
{
"code": "EUR",
"name": "Euro",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "GNO",
"name": "GNO",
"statuses": [
"primary"
]
},
{
"code": "GNT",
"name": "GNT",
"statuses": [
"primary"
]
},
{
"code": "JPY",
"name": "Japanese Yen",
"statuses": [
"secondary"
]
},
{
"code": "LTC",
"name": "Litecoin",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "MAID",
"name": "MaidSafeCoin",
"statuses": [
"primary"
]
},
{
"code": "XEM",
"name": "NEM",
"statuses": [
"primary"
]
},
{
"code": "XLM",
"name": "Stellar",
"statuses": [
"primary"
]
},
{
"code": "XMR",
"name": "Monero",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "XRP",
"name": "Ripple",
"statuses": [
"primary"
]
},
{
"code": "RUR",
"name": "Ruble",
"statuses": [
"secondary"
]
},
{
"code": "STEEM",
"name": "Steem",
"statuses": [
"primary"
]
},
{
"code": "STRAT",
"name": "STRAT",
"statuses": [
"primary"
]
},
{
"code": "UAH",
"name": "Ukrainian Hryvnia",
"statuses": [
"secondary"
]
},
{
"code": "USD",
"name": "US Dollar",
"statuses": [
"primary",
"secondary"
]
},
{
"code": "WAVES",
"name": "WAVES",
"statuses": [
"primary"
]
},
{
"code": "ZEC",
"name": "Zcash",
"statuses": [
"primary"
]
}
]
}

View File

@ -0,0 +1,653 @@
{
"objects": [
{
"symbol": "ethaud",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "aud",
"name": "Australian Dollar"
}
},
{
"symbol": "ethhkd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "hkd",
"name": "Hong Kong Dollar"
}
},
{
"symbol": "ethsgd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "sgd",
"name": "Singapore Dollar"
}
},
{
"symbol": "ethidr",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "idr",
"name": "Indonesian Rupiah"
}
},
{
"symbol": "ethphp",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "php",
"name": "Philippine Peso"
}
},
{
"symbol": "eth1st",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "1st",
"name": "FirstBlood"
}
},
{
"symbol": "ethadt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "adt",
"name": "adToken"
}
},
{
"symbol": "ethadx",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "adx",
"name": "AdEx"
}
},
{
"symbol": "ethant",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "ant",
"name": "Aragon"
}
},
{
"symbol": "ethbat",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "bat",
"name": "Basic Attention Token"
}
},
{
"symbol": "ethbnt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "bnt",
"name": "Bancor"
}
},
{
"symbol": "ethbtc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "btc",
"name": "Bitcoin"
}
},
{
"symbol": "ethcad",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "cad",
"name": "Canadian Dollar"
}
},
{
"symbol": "ethcfi",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "cfi",
"name": "Cofound.it"
}
},
{
"symbol": "ethcrb",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "crb",
"name": "CreditBit"
}
},
{
"symbol": "ethcvc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "cvc",
"name": "Civic"
}
},
{
"symbol": "ethdash",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "dash",
"name": "Dash"
}
},
{
"symbol": "ethdgd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "dgd",
"name": "DigixDAO"
}
},
{
"symbol": "ethetc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "etc",
"name": "Ethereum Classic"
}
},
{
"symbol": "etheur",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "eur",
"name": "Euro"
}
},
{
"symbol": "ethfun",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "fun",
"name": "FunFair"
}
},
{
"symbol": "ethgbp",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gbp",
"name": "Pound Sterling"
}
},
{
"symbol": "ethgno",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gno",
"name": "Gnosis"
}
},
{
"symbol": "ethgnt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gnt",
"name": "Golem"
}
},
{
"symbol": "ethgup",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gup",
"name": "Matchpool"
}
},
{
"symbol": "ethhmq",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "hmq",
"name": "Humaniq"
}
},
{
"symbol": "ethjpy",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "jpy",
"name": "Japanese Yen"
}
},
{
"symbol": "ethlgd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "lgd",
"name": "Legends Room"
}
},
{
"symbol": "ethlsk",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "lsk",
"name": "Lisk"
}
},
{
"symbol": "ethltc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "ltc",
"name": "Litecoin"
}
},
{
"symbol": "ethlun",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "lun",
"name": "Lunyr"
}
},
{
"symbol": "ethmco",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "mco",
"name": "Monaco"
}
},
{
"symbol": "ethmtl",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "mtl",
"name": "Metal"
}
},
{
"symbol": "ethmyst",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "myst",
"name": "Mysterium"
}
},
{
"symbol": "ethnmr",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "nmr",
"name": "Numeraire"
}
},
{
"symbol": "ethomg",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "omg",
"name": "OmiseGO"
}
},
{
"symbol": "ethpay",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "pay",
"name": "TenX"
}
},
{
"symbol": "ethptoy",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "ptoy",
"name": "Patientory"
}
},
{
"symbol": "ethqrl",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "qrl",
"name": "Quantum-Resistant Ledger"
}
},
{
"symbol": "ethqtum",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "qtum",
"name": "Qtum"
}
},
{
"symbol": "ethrep",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "rep",
"name": "Augur"
}
},
{
"symbol": "ethrlc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "rlc",
"name": "iEx.ec"
}
},
{
"symbol": "ethrub",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "rub",
"name": "Russian Ruble"
}
},
{
"symbol": "ethsc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "sc",
"name": "Siacoin"
}
},
{
"symbol": "ethsngls",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "sngls",
"name": "SingularDTV"
}
},
{
"symbol": "ethsnt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "snt",
"name": "Status"
}
},
{
"symbol": "ethsteem",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "steem",
"name": "Steem"
}
},
{
"symbol": "ethstorj",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "storj",
"name": "Storj"
}
},
{
"symbol": "ethtime",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "time",
"name": "ChronoBank"
}
},
{
"symbol": "ethtkn",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "tkn",
"name": "TokenCard"
}
},
{
"symbol": "ethtrst",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "trst",
"name": "WeTrust"
}
},
{
"symbol": "ethuah",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "uah",
"name": "Ukrainian Hryvnia"
}
},
{
"symbol": "ethusd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "usd",
"name": "United States Dollar"
}
},
{
"symbol": "ethwings",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "wings",
"name": "Wings"
}
},
{
"symbol": "ethxem",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xem",
"name": "NEM"
}
},
{
"symbol": "ethxlm",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xlm",
"name": "Stellar Lumen"
}
},
{
"symbol": "ethxmr",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xmr",
"name": "Monero"
}
},
{
"symbol": "ethxrp",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xrp",
"name": "Ripple"
}
},
{
"symbol": "ethzec",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "zec",
"name": "Zcash"
}
}
]
}