aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorDan Finlay <flyswatter@users.noreply.github.com>2017-01-03 06:04:27 +0800
committerGitHub <noreply@github.com>2017-01-03 06:04:27 +0800
commit25d0ac147668e45da43a4dd74876e3e22bd6a246 (patch)
tree6a81531e5398afc26685e7b0523c7526dabd0f7f /app
parentedc5f9e821bf18fa6ec984e645790fba3457d0bb (diff)
parentfb002dc44eae82e0471b8b52acecc61ba5f39332 (diff)
downloadtangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.tar
tangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.tar.gz
tangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.tar.bz2
tangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.tar.lz
tangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.tar.xz
tangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.tar.zst
tangerine-wallet-browser-25d0ac147668e45da43a4dd74876e3e22bd6a246.zip
Merge branch 'dev' into ShowDifferentAccounts
Diffstat (limited to 'app')
-rw-r--r--app/scripts/background.js13
-rw-r--r--app/scripts/keyring-controller.js207
-rw-r--r--app/scripts/lib/config-manager.js54
-rw-r--r--app/scripts/lib/idStore.js254
-rw-r--r--app/scripts/lib/inpage-provider.js2
-rw-r--r--app/scripts/lib/tx-utils.js87
-rw-r--r--app/scripts/metamask-controller.js35
-rw-r--r--app/scripts/transaction-manager.js288
8 files changed, 421 insertions, 519 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 7cb25d8bf..ca2efc114 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -17,13 +17,13 @@ const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
- showUnconfirmedTx: triggerUi,
+ showUnapprovedTx: triggerUi,
// Persistence Methods:
setData,
loadData,
})
const keyringController = controller.keyringController
-
+const txManager = controller.txManager
function triggerUi () {
if (!popupIsOpen) notification.show()
}
@@ -97,15 +97,14 @@ function setupControllerConnection (stream) {
// plugin badge text
//
-keyringController.on('update', updateBadge)
+txManager.on('updateBadge', updateBadge)
function updateBadge () {
var label = ''
- var unconfTxs = controller.configManager.unconfirmedTxs()
- var unconfTxLen = Object.keys(unconfTxs).length
+ var unapprovedTxCount = controller.txManager.unapprovedTxCount
var unconfMsgs = messageManager.unconfirmedMsgs()
var unconfMsgLen = Object.keys(unconfMsgs).length
- var count = unconfTxLen + unconfMsgLen
+ var count = unapprovedTxCount + unconfMsgLen
if (count) {
label = String(count)
}
@@ -113,6 +112,8 @@ function updateBadge () {
extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
}
+// data :: setters/getters
+
function loadData () {
var oldData = getOldStyleData()
var newData
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index d53de1ab6..05c4a26fa 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -1,8 +1,5 @@
-const async = require('async')
const ethUtil = require('ethereumjs-util')
-const EthQuery = require('eth-query')
const bip39 = require('bip39')
-const Transaction = require('ethereumjs-tx')
const EventEmitter = require('events').EventEmitter
const filter = require('promise-filter')
const encryptor = require('browser-passworder')
@@ -36,11 +33,9 @@ module.exports = class KeyringController extends EventEmitter {
this.ethStore = opts.ethStore
this.encryptor = encryptor
this.keyringTypes = keyringTypes
-
this.keyrings = []
this.identities = {} // Essentially a name hash
- this._unconfTxCbs = {}
this._unconfMsgCbs = {}
this.getNetwork = opts.getNetwork
@@ -100,7 +95,6 @@ module.exports = class KeyringController extends EventEmitter {
isInitialized: (!!wallet || !!vault),
isUnlocked: Boolean(this.password),
isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(),
- unconfTxs: this.configManager.unconfirmedTxs(),
transactions: this.configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
@@ -116,7 +110,6 @@ module.exports = class KeyringController extends EventEmitter {
})
}
-
// Create New Vault And Keychain
// @string password - The password to encrypt the vault with
//
@@ -319,202 +312,18 @@ module.exports = class KeyringController extends EventEmitter {
}
- // SIGNING RELATED METHODS
- //
- // SIGN, SUBMIT TX, CANCEL, AND APPROVE.
- // THIS SECTION INVOLVES THE REQUEST, STORING, AND SIGNING OF DATA
- // WITH THE KEYS STORED IN THIS CONTROLLER.
-
-
- // Add Unconfirmed Transaction
- // @object txParams
- // @function onTxDoneCb
- // @function cb
+ // SIGNING METHODS
//
- // Calls back `cb` with @object txData = { txParams }
- // Calls back `onTxDoneCb` with `true` or an `error` depending on result.
- //
- // Prepares the given `txParams` for final confirmation and approval.
- // Estimates gas and other preparatory steps.
- // Caches the requesting Dapp's callback, `onTxDoneCb`, for resolution later.
- addUnconfirmedTransaction (txParams, onTxDoneCb, cb) {
- const configManager = this.configManager
-
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var txId = createId()
- txParams.metamaskId = txId
- txParams.metamaskNetworkId = this.getNetwork()
- var txData = {
- id: txId,
- txParams: txParams,
- time: time,
- status: 'unconfirmed',
- gasMultiplier: configManager.getGasMultiplier() || 1,
- metamaskNetworkId: this.getNetwork(),
- }
-
- // keep the onTxDoneCb around for after approval/denial (requires user interaction)
- // This onTxDoneCb fires completion to the Dapp's write operation.
- this._unconfTxCbs[txId] = onTxDoneCb
-
- var provider = this.ethStore._query.currentProvider
- var query = new EthQuery(provider)
-
- // calculate metadata for tx
- this.analyzeTxGasUsage(query, txData, this.txDidComplete.bind(this, txData, cb))
- }
-
- estimateTxGas (query, txData, blockGasLimitHex, cb) {
- const txParams = txData.txParams
- // check if gasLimit is already specified
- txData.gasLimitSpecified = Boolean(txParams.gas)
- // if not, fallback to block gasLimit
- if (!txData.gasLimitSpecified) {
- txParams.gas = blockGasLimitHex
- }
- // run tx, see if it will OOG
- query.estimateGas(txParams, cb)
- }
-
- checkForTxGasError (txData, estimatedGasHex, cb) {
- txData.estimatedGas = estimatedGasHex
- // all gas used - must be an error
- if (estimatedGasHex === txData.txParams.gas) {
- txData.simulationFails = true
- }
- cb()
- }
-
- setTxGas (txData, blockGasLimitHex, cb) {
- const txParams = txData.txParams
- // if OOG, nothing more to do
- if (txData.simulationFails) {
- cb()
- return
- }
- // if gasLimit was specified and doesnt OOG,
- // use original specified amount
- if (txData.gasLimitSpecified) {
- txData.estimatedGas = txParams.gas
- cb()
- return
- }
- // if gasLimit not originally specified,
- // try adding an additional gas buffer to our estimation for safety
- const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16)
- const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16)
- const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16)
- // added gas buffer is too high
- if (estimationWithBuffer.gt(blockGasLimitBn)) {
- txParams.gas = txData.estimatedGas
- // added gas buffer is safe
- } else {
- const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
- txParams.gas = gasWithBufferHex
- }
- cb()
- return
- }
-
- txDidComplete (txData, cb, err) {
- if (err) return cb(err)
- const configManager = this.configManager
- configManager.addTx(txData)
- // signal update
- this.emit('update')
- // signal completion of add tx
- cb(null, txData)
- }
-
- analyzeTxGasUsage (query, txData, cb) {
- query.getBlockByNumber('latest', true, (err, block) => {
- if (err) return cb(err)
- async.waterfall([
- this.estimateTxGas.bind(this, query, txData, block.gasLimit),
- this.checkForTxGasError.bind(this, txData),
- this.setTxGas.bind(this, txData, block.gasLimit),
- ], cb)
- })
- }
-
- // Cancel Transaction
- // @string txId
- // @function cb
- //
- // Calls back `cb` with no error if provided.
- //
- // Forgets any tx matching `txId`.
- cancelTransaction (txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- configManager.rejectTx(txId)
- delete this._unconfTxCbs[txId]
-
- if (cb && typeof cb === 'function') {
- cb()
- }
- }
-
- // Approve Transaction
- // @string txId
- // @function cb
- //
- // Calls back `cb` with no error always.
- //
- // Attempts to sign a Transaction with `txId`
- // and submit it to the blockchain.
- //
- // Calls back the cached Dapp's confirmation callback, also.
- approveTransaction (txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // accept tx
- cb()
- approvalCb(null, true)
- // clean up
- configManager.confirmTx(txId)
- delete this._unconfTxCbs[txId]
- this.emit('update')
- }
-
- signTransaction (txParams, cb) {
+ // This method signs tx and returns a promise for
+ // TX Manager to update the state after signing
+ signTransaction (ethTx, selectedAddress, txId, cb) {
try {
- const address = normalize(txParams.from)
+ const address = normalize(selectedAddress)
return this.getKeyringForAccount(address)
.then((keyring) => {
- // Handle gas pricing
- var gasMultiplier = this.configManager.getGasMultiplier() || 1
- var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
- gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
- txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
-
- // normalize values
- txParams.to = normalize(txParams.to)
- txParams.from = normalize(txParams.from)
- txParams.value = normalize(txParams.value)
- txParams.data = normalize(txParams.data)
- txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
- txParams.nonce = normalize(txParams.nonce)
-
- const tx = new Transaction(txParams)
- return keyring.signTransaction(address, tx)
- })
- .then((tx) => {
- // Add the tx hash to the persisted meta-tx object
- var txHash = ethUtil.bufferToHex(tx.hash())
- var metaTx = this.configManager.getTx(txParams.metamaskId)
- metaTx.hash = txHash
- this.configManager.updateTx(metaTx)
-
- // return raw serialized tx
- var rawTx = ethUtil.bufferToHex(tx.serialize())
- cb(null, rawTx)
+ return keyring.signTransaction(address, ethTx)
+ }).then((tx) => {
+ this.emit(`${txId}:signed`, {tx, txId, cb})
})
} catch (e) {
cb(e)
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index ede877b76..93501c859 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -8,7 +8,6 @@ const normalize = require('./sig-util').normalize
const TESTNET_RPC = MetamaskConfig.network.testnet
const MAINNET_RPC = MetamaskConfig.network.mainnet
const MORDEN_RPC = MetamaskConfig.network.morden
-const txLimit = 40
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@@ -19,8 +18,6 @@ const txLimit = 40
*/
module.exports = ConfigManager
function ConfigManager (opts) {
- this.txLimit = txLimit
-
// ConfigManager is observable and will emit updates
this._subs = []
@@ -209,61 +206,12 @@ ConfigManager.prototype.getTxList = function () {
}
}
-ConfigManager.prototype.unconfirmedTxs = function () {
- var transactions = this.getTxList()
- return transactions.filter(tx => tx.status === 'unconfirmed')
- .reduce((result, tx) => { result[tx.id] = tx; return result }, {})
-}
-
-ConfigManager.prototype._saveTxList = function (txList) {
+ConfigManager.prototype.setTxList = function (txList) {
var data = this.migrator.getData()
data.transactions = txList
this.setData(data)
}
-ConfigManager.prototype.addTx = function (tx) {
- var transactions = this.getTxList()
- while (transactions.length > this.txLimit - 1) {
- transactions.shift()
- }
- transactions.push(tx)
- this._saveTxList(transactions)
-}
-
-ConfigManager.prototype.getTx = function (txId) {
- var transactions = this.getTxList()
- var matching = transactions.filter(tx => tx.id === txId)
- return matching.length > 0 ? matching[0] : null
-}
-
-ConfigManager.prototype.confirmTx = function (txId) {
- this._setTxStatus(txId, 'confirmed')
-}
-
-ConfigManager.prototype.rejectTx = function (txId) {
- this._setTxStatus(txId, 'rejected')
-}
-
-ConfigManager.prototype._setTxStatus = function (txId, status) {
- var tx = this.getTx(txId)
- tx.status = status
- this.updateTx(tx)
-}
-
-ConfigManager.prototype.updateTx = function (tx) {
- var transactions = this.getTxList()
- var found, index
- transactions.forEach((otherTx, i) => {
- if (otherTx.id === tx.id) {
- found = true
- index = i
- }
- })
- if (found) {
- transactions[index] = tx
- }
- this._saveTxList(transactions)
-}
// wallet nickname methods
diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js
index 66e5d966c..e4cbca456 100644
--- a/app/scripts/lib/idStore.js
+++ b/app/scripts/lib/idStore.js
@@ -1,19 +1,14 @@
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
-const async = require('async')
const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const EthQuery = require('eth-query')
const KeyStore = require('eth-lightwallet').keystore
const clone = require('clone')
const extend = require('xtend')
-const createId = require('./random-id')
-const ethBinToOps = require('eth-bin-to-ops')
const autoFaucet = require('./auto-faucet')
-const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
const IdManagement = require('./id-management')
+
module.exports = IdentityStore
inherits(IdentityStore, EventEmitter)
@@ -34,10 +29,7 @@ function IdentityStore (opts = {}) {
selectedAddress: null,
identities: {},
}
-
// not part of serilized metamask state - only kept in memory
- this._unconfTxCbs = {}
- this._unconfMsgCbs = {}
}
//
@@ -103,10 +95,6 @@ IdentityStore.prototype.getState = function () {
isUnlocked: this._isUnlocked(),
seedWords: seedWords,
isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(),
- unconfTxs: configManager.unconfirmedTxs(),
- transactions: configManager.getTxList(),
- unconfMsgs: messageManager.unconfirmedMsgs(),
- messages: messageManager.getMsgList(),
selectedAddress: configManager.getSelectedAccount(),
shapeShiftTxList: configManager.getShapeShiftTxList(),
currentFiat: configManager.getCurrentFiat(),
@@ -206,245 +194,6 @@ IdentityStore.prototype.exportAccount = function (address, cb) {
return privateKey
}
-//
-// Transactions
-//
-
-// comes from dapp via zero-client hooked-wallet provider
-IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) {
- const configManager = this.configManager
-
- var self = this
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var txId = createId()
- txParams.metamaskId = txId
- txParams.metamaskNetworkId = self._currentState.network
- var txData = {
- id: txId,
- txParams: txParams,
- time: time,
- status: 'unconfirmed',
- gasMultiplier: configManager.getGasMultiplier() || 1,
- }
-
- console.log('addUnconfirmedTransaction:', txData)
-
- // keep the onTxDoneCb around for after approval/denial (requires user interaction)
- // This onTxDoneCb fires completion to the Dapp's write operation.
- self._unconfTxCbs[txId] = onTxDoneCb
-
- var provider = self._ethStore._query.currentProvider
- var query = new EthQuery(provider)
-
- // calculate metadata for tx
- async.parallel([
- analyzeForDelegateCall,
- estimateGas,
- ], didComplete)
-
- // perform static analyis on the target contract code
- function analyzeForDelegateCall (cb) {
- if (txParams.to) {
- query.getCode(txParams.to, (err, result) => {
- if (err) return cb(err.message || err)
- var containsDelegateCall = self.checkForDelegateCall(result)
- txData.containsDelegateCall = containsDelegateCall
- cb()
- })
- } else {
- cb()
- }
- }
-
- function estimateGas (cb) {
- var estimationParams = extend(txParams)
- query.getBlockByNumber('latest', true, function (err, block) {
- if (err) return cb(err)
- // check if gasLimit is already specified
- const gasLimitSpecified = Boolean(txParams.gas)
- // if not, fallback to block gasLimit
- if (!gasLimitSpecified) {
- estimationParams.gas = block.gasLimit
- }
- // run tx, see if it will OOG
- query.estimateGas(estimationParams, function (err, estimatedGasHex) {
- if (err) return cb(err.message || err)
- // all gas used - must be an error
- if (estimatedGasHex === estimationParams.gas) {
- txData.simulationFails = true
- txData.estimatedGas = estimatedGasHex
- txData.txParams.gas = estimatedGasHex
- cb()
- return
- }
- // otherwise, did not use all gas, must be ok
-
- // if specified gasLimit and no error, we're done
- if (gasLimitSpecified) {
- txData.estimatedGas = txParams.gas
- cb()
- return
- }
-
- // try adding an additional gas buffer to our estimation for safety
- const estimatedGasBn = new BN(ethUtil.stripHexPrefix(estimatedGasHex), 16)
- const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(block.gasLimit), 16)
- const estimationWithBuffer = self.addGasBuffer(estimatedGasBn)
- // added gas buffer is too high
- if (estimationWithBuffer.gt(blockGasLimitBn)) {
- txData.estimatedGas = estimatedGasHex
- txData.txParams.gas = estimatedGasHex
- // added gas buffer is safe
- } else {
- const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
- txData.estimatedGas = gasWithBufferHex
- txData.txParams.gas = gasWithBufferHex
- }
- cb()
- return
- })
- })
- }
-
- function didComplete (err) {
- if (err) return cb(err.message || err)
- configManager.addTx(txData)
- // signal update
- self._didUpdate()
- // signal completion of add tx
- cb(null, txData)
- }
-}
-
-IdentityStore.prototype.checkForDelegateCall = function (codeHex) {
- const code = ethUtil.toBuffer(codeHex)
- if (code !== '0x') {
- const ops = ethBinToOps(code)
- const containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL')
- return containsDelegateCall
- } else {
- return false
- }
-}
-
-IdentityStore.prototype.addGasBuffer = function (gasBn) {
- // add 20% to specified gas
- const gasBuffer = gasBn.div(new BN('5', 10))
- const gasWithBuffer = gasBn.add(gasBuffer)
- return gasWithBuffer
-}
-
-// comes from metamask ui
-IdentityStore.prototype.approveTransaction = function (txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // accept tx
- cb()
- approvalCb(null, true)
- // clean up
- configManager.confirmTx(txId)
- delete this._unconfTxCbs[txId]
- this._didUpdate()
-}
-
-// comes from metamask ui
-IdentityStore.prototype.cancelTransaction = function (txId) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- configManager.rejectTx(txId)
- delete this._unconfTxCbs[txId]
- this._didUpdate()
-}
-
-// performs the actual signing, no autofill of params
-IdentityStore.prototype.signTransaction = function (txParams, cb) {
- try {
- console.log('signing tx...', txParams)
- var rawTx = this._idmgmt.signTx(txParams)
- cb(null, rawTx)
- } catch (err) {
- cb(err)
- }
-}
-
-//
-// Messages
-//
-
-// comes from dapp via zero-client hooked-wallet provider
-IdentityStore.prototype.addUnconfirmedMessage = function (msgParams, cb) {
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var msgId = createId()
- var msgData = {
- id: msgId,
- msgParams: msgParams,
- time: time,
- status: 'unconfirmed',
- }
- messageManager.addMsg(msgData)
- console.log('addUnconfirmedMessage:', msgData)
-
- // keep the cb around for after approval (requires user interaction)
- // This cb fires completion to the Dapp's write operation.
- this._unconfMsgCbs[msgId] = cb
-
- // signal update
- this._didUpdate()
-
- return msgId
-}
-
-// comes from metamask ui
-IdentityStore.prototype.approveMessage = function (msgId, cb) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // accept msg
- cb()
- approvalCb(null, true)
- // clean up
- messageManager.confirmMsg(msgId)
- delete this._unconfMsgCbs[msgId]
- this._didUpdate()
-}
-
-// comes from metamask ui
-IdentityStore.prototype.cancelMessage = function (msgId) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- messageManager.rejectMsg(msgId)
- delete this._unconfTxCbs[msgId]
- this._didUpdate()
-}
-
-// performs the actual signing, no autofill of params
-IdentityStore.prototype.signMessage = function (msgParams, cb) {
- try {
- console.log('signing msg...', msgParams.data)
- var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
- if ('metamaskId' in msgParams) {
- var id = msgParams.metamaskId
- delete msgParams.metamaskId
-
- this.approveMessage(id, cb)
- } else {
- cb(null, rawMsg)
- }
- } catch (err) {
- cb(err)
- }
-}
-
-//
// private
//
@@ -599,4 +348,3 @@ IdentityStore.prototype._autoFaucet = function () {
// util
-function noop () {}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index a64c745ce..11bd5cc3a 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -111,6 +111,8 @@ MetamaskInpageProvider.prototype.isConnected = function () {
return true
}
+MetamaskInpageProvider.prototype.isMetaMask = true
+
// util
function remoteStoreWithLocalStorageCache (storageKey) {
diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js
new file mode 100644
index 000000000..a976173f5
--- /dev/null
+++ b/app/scripts/lib/tx-utils.js
@@ -0,0 +1,87 @@
+const async = require('async')
+const EthQuery = require('eth-query')
+const ethUtil = require('ethereumjs-util')
+const BN = ethUtil.BN
+
+/*
+tx-utils are utility methods for Transaction manager
+its passed a provider and that is passed to ethquery
+and used to do things like calculate gas of a tx.
+*/
+
+module.exports = class txProviderUtils {
+ constructor (provider) {
+ this.provider = provider
+ this.query = new EthQuery(provider)
+ }
+ analyzeGasUsage (txData, cb) {
+ var self = this
+ this.query.getBlockByNumber('latest', true, (err, block) => {
+ if (err) return cb(err)
+ async.waterfall([
+ self.estimateTxGas.bind(self, txData, block.gasLimit),
+ self.checkForTxGasError.bind(self, txData),
+ self.setTxGas.bind(self, txData, block.gasLimit),
+ ], cb)
+ })
+ }
+
+ estimateTxGas (txData, blockGasLimitHex, cb) {
+ const txParams = txData.txParams
+ // check if gasLimit is already specified
+ txData.gasLimitSpecified = Boolean(txParams.gas)
+ // if not, fallback to block gasLimit
+ if (!txData.gasLimitSpecified) {
+ txParams.gas = blockGasLimitHex
+ }
+ // run tx, see if it will OOG
+ this.query.estimateGas(txParams, cb)
+ }
+
+ checkForTxGasError (txData, estimatedGasHex, cb) {
+ txData.estimatedGas = estimatedGasHex
+ // all gas used - must be an error
+ if (estimatedGasHex === txData.txParams.gas) {
+ txData.simulationFails = true
+ }
+ cb()
+ }
+
+ setTxGas (txData, blockGasLimitHex, cb) {
+ const txParams = txData.txParams
+ // if OOG, nothing more to do
+ if (txData.simulationFails) {
+ cb()
+ return
+ }
+ // if gasLimit was specified and doesnt OOG,
+ // use original specified amount
+ if (txData.gasLimitSpecified) {
+ txData.estimatedGas = txParams.gas
+ cb()
+ return
+ }
+ // if gasLimit not originally specified,
+ // try adding an additional gas buffer to our estimation for safety
+ const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16)
+ const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16)
+ const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16)
+ // added gas buffer is too high
+ if (estimationWithBuffer.gt(blockGasLimitBn)) {
+ txParams.gas = txData.estimatedGas
+ // added gas buffer is safe
+ } else {
+ const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
+ txParams.gas = gasWithBufferHex
+ }
+ cb()
+ return
+ }
+
+ 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))
+ }
+}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 8d4d574ff..3e27272b9 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -4,6 +4,7 @@ const MetaMaskProvider = require('web3-provider-engine/zero.js')
const KeyringController = require('./keyring-controller')
const NoticeController = require('./notice-controller')
const messageManager = require('./lib/message-manager')
+const TxManager = require('./transaction-manager')
const HostStore = require('./lib/remote-store.js').HostStore
const Web3 = require('web3')
const ConfigManager = require('./lib/config-manager')
@@ -12,7 +13,6 @@ const autoFaucet = require('./lib/auto-faucet')
const nodeify = require('./lib/nodeify')
const IdStoreMigrator = require('./lib/idStore-migrator')
-
module.exports = class MetamaskController {
constructor (opts) {
@@ -36,6 +36,16 @@ module.exports = class MetamaskController {
this.keyringController.setStore(this.ethStore)
this.getNetwork()
this.messageManager = messageManager
+ this.txManager = new TxManager({
+ txList: this.configManager.getTxList(),
+ txHistoryLimit: 40,
+ setTxList: this.configManager.setTxList.bind(this.configManager),
+ getSelectedAccount: this.configManager.getSelectedAccount.bind(this.configManager),
+ getGasMultiplier: this.configManager.getGasMultiplier.bind(this.configManager),
+ getNetwork: this.getStateNetwork.bind(this),
+ provider: this.provider,
+ blockTracker: this.provider,
+ })
this.publicConfigStore = this.initPublicConfigStore()
var currentFiat = this.configManager.getCurrentFiat() || 'USD'
@@ -59,6 +69,7 @@ module.exports = class MetamaskController {
this.state,
this.ethStore.getState(),
this.configManager.getConfig(),
+ this.txManager.getState(),
keyringControllerState,
this.noticeController.getState(), {
lostAccounts: this.configManager.getLostAccounts(),
@@ -69,6 +80,7 @@ module.exports = class MetamaskController {
getApi () {
const keyringController = this.keyringController
+ const txManager = this.txManager
const noticeController = this.noticeController
return {
@@ -103,8 +115,8 @@ module.exports = class MetamaskController {
exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
// signing methods
- approveTransaction: keyringController.approveTransaction.bind(keyringController),
- cancelTransaction: keyringController.cancelTransaction.bind(keyringController),
+ approveTransaction: txManager.approveTransaction.bind(txManager),
+ cancelTransaction: txManager.cancelTransaction.bind(txManager),
signMessage: keyringController.signMessage.bind(keyringController),
cancelMessage: keyringController.cancelMessage.bind(keyringController),
@@ -168,7 +180,8 @@ module.exports = class MetamaskController {
// tx signing
approveTransaction: this.newUnsignedTransaction.bind(this),
signTransaction: (...args) => {
- keyringController.signTransaction(...args)
+ this.setupSigningListners(...args)
+ this.txManager.formatTxForSigining(...args)
this.sendUpdate()
},
@@ -184,7 +197,6 @@ module.exports = class MetamaskController {
var web3 = new Web3(provider)
this.web3 = web3
keyringController.web3 = web3
-
provider.on('block', this.processBlock.bind(this))
provider.on('error', this.getNetwork.bind(this))
@@ -238,16 +250,23 @@ module.exports = class MetamaskController {
}
newUnsignedTransaction (txParams, onTxDoneCb) {
- const keyringController = this.keyringController
+ const txManager = this.txManager
const err = this.enforceTxValidations(txParams)
if (err) return onTxDoneCb(err)
- keyringController.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => {
+ txManager.addUnapprovedTransaction(txParams, onTxDoneCb, (err, txData) => {
if (err) return onTxDoneCb(err)
this.sendUpdate()
- this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb)
+ this.opts.showUnapprovedTx(txParams, txData, onTxDoneCb)
})
}
+ setupSigningListners (txParams) {
+ var txId = txParams.metamaskId
+ // apply event listeners for signing and formating events
+ this.txManager.once(`${txId}:formatted`, this.keyringController.signTransaction.bind(this.keyringController))
+ this.keyringController.once(`${txId}:signed`, this.txManager.resolveSignedTransaction.bind(this.txManager))
+ }
+
enforceTxValidations (txParams) {
if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
const msg = `Invalid transaction value of ${txParams.value} not a positive number.`
diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js
new file mode 100644
index 000000000..fd136a51b
--- /dev/null
+++ b/app/scripts/transaction-manager.js
@@ -0,0 +1,288 @@
+const EventEmitter = require('events')
+const extend = require('xtend')
+const ethUtil = require('ethereumjs-util')
+const Transaction = require('ethereumjs-tx')
+const BN = ethUtil.BN
+const TxProviderUtil = require('./lib/tx-utils')
+const createId = require('./lib/random-id')
+const normalize = require('./lib/sig-util').normalize
+
+module.exports = class TransactionManager extends EventEmitter {
+ constructor (opts) {
+ super()
+ this.txList = opts.txList || []
+ this._setTxList = opts.setTxList
+ this.txHistoryLimit = opts.txHistoryLimit
+ this.getSelectedAccount = opts.getSelectedAccount
+ this.provider = opts.provider
+ this.blockTracker = opts.blockTracker
+ this.txProviderUtils = new TxProviderUtil(this.provider)
+ this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
+ this.getGasMultiplier = opts.getGasMultiplier
+ this.getNetwork = opts.getNetwork
+ }
+
+ getState () {
+ var selectedAccount = this.getSelectedAccount()
+ return {
+ transactions: this.getTxList(),
+ unconfTxs: this.getUnapprovedTxList(),
+ selectedAccountTxList: this.getFilteredTxList({metamaskNetworkId: this.getNetwork(), from: selectedAccount}),
+ }
+ }
+
+// Returns the tx list
+ getTxList () {
+ return this.txList
+ }
+
+ // Adds a tx to the txlist
+ addTx (txMeta, onTxDoneCb = warn) {
+ var txList = this.getTxList()
+ var txHistoryLimit = this.txHistoryLimit
+
+ // checks if the length of th 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 (txList.length > txHistoryLimit - 1) {
+ var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
+ txList.splice(index, 1)
+ }
+ txList.push(txMeta)
+
+ this._saveTxList(txList)
+ // keep the onTxDoneCb around in a listener
+ // for after approval/denial (requires user interaction)
+ // This onTxDoneCb fires completion to the Dapp's write operation.
+ this.once(`${txMeta.id}:signed`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:rejected`)
+ onTxDoneCb(null, true)
+ })
+ this.once(`${txMeta.id}:rejected`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:signed`)
+ onTxDoneCb(null, false)
+ })
+
+ this.emit('updateBadge')
+ this.emit(`${txMeta.id}:unapproved`, txMeta)
+ }
+
+ // gets tx by Id and returns it
+ getTx (txId, cb) {
+ var txList = this.getTxList()
+ var txMeta = txList.find(txData => txData.id === txId)
+ return cb ? cb(txMeta) : txMeta
+ }
+
+ //
+ updateTx (txMeta) {
+ var txId = txMeta.id
+ var txList = this.getTxList()
+ var index = txList.findIndex(txData => txData.id === txId)
+ txList[index] = txMeta
+ this._saveTxList(txList)
+ }
+
+ get unapprovedTxCount () {
+ return Object.keys(this.getUnapprovedTxList()).length
+ }
+
+ get pendingTxCount () {
+ return this.getTxsByMetaData('status', 'signed').length
+ }
+
+ addUnapprovedTransaction (txParams, onTxDoneCb, cb) {
+ // create txData obj with parameters and meta data
+ var time = (new Date()).getTime()
+ var txId = createId()
+ txParams.metamaskId = txId
+ txParams.metamaskNetworkId = this.getNetwork()
+ var txData = {
+ id: txId,
+ txParams: txParams,
+ time: time,
+ status: 'unapproved',
+ gasMultiplier: this.getGasMultiplier() || 1,
+ metamaskNetworkId: this.getNetwork(),
+ }
+ this.txProviderUtils.analyzeGasUsage(txData, this.txDidComplete.bind(this, txData, onTxDoneCb, cb))
+ // calculate metadata for tx
+ }
+
+ txDidComplete (txMeta, onTxDoneCb, cb, err) {
+ if (err) return cb(err)
+ this.addTx(txMeta, onTxDoneCb)
+ cb(null, txMeta)
+ }
+
+ getUnapprovedTxList () {
+ var txList = this.getTxList()
+ return txList.filter((txMeta) => txMeta.status === 'unapproved')
+ .reduce((result, tx) => {
+ result[tx.id] = tx
+ return result
+ }, {})
+ }
+
+ approveTransaction (txId, cb = warn) {
+ this.setTxStatusSigned(txId)
+ cb()
+ }
+
+ cancelTransaction (txId, cb = warn) {
+ this.setTxStatusRejected(txId)
+ cb()
+ }
+
+ // formats txParams so the keyringController can sign it
+ formatTxForSigining (txParams, cb) {
+ var address = txParams.from
+ var metaTx = this.getTx(txParams.metamaskId)
+ var gasMultiplier = metaTx.gasMultiplier
+ var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
+ txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
+
+ // normalize values
+ txParams.to = normalize(txParams.to)
+ txParams.from = normalize(txParams.from)
+ txParams.value = normalize(txParams.value)
+ txParams.data = normalize(txParams.data)
+ txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
+ txParams.nonce = normalize(txParams.nonce)
+ const ethTx = new Transaction(txParams)
+
+ // listener is assigned in metamaskController
+ this.emit(`${txParams.metamaskId}:formatted`, ethTx, address, txParams.metamaskId, cb)
+ }
+
+ // receives a signed tx object and updates the tx hash
+ // and pass it to the cb to be sent off
+ resolveSignedTransaction ({tx, txId, cb = warn}) {
+ // Add the tx hash to the persisted meta-tx object
+ var txHash = ethUtil.bufferToHex(tx.hash())
+ var metaTx = this.getTx(txId)
+ metaTx.hash = txHash
+ this.updateTx(metaTx)
+ var rawTx = ethUtil.bufferToHex(tx.serialize())
+ cb(null, rawTx)
+ }
+
+ /*
+ Takes an object of fields to search for eg:
+ var thingsToLookFor = {
+ to: '0x0..',
+ from: '0x0..',
+ status: 'signed',
+ }
+ and returns a list of tx with all
+ options matching
+
+ 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) {
+ var 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 (key in txMeta.txParams) {
+ 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 'signed'.
+ setTxStatusSigned (txId) {
+ this._setTxStatus(txId, 'signed')
+ this.emit('updateBadge')
+ }
+
+ // should update the status of the tx to 'rejected'.
+ setTxStatusRejected (txId) {
+ this._setTxStatus(txId, 'rejected')
+ this.emit('updateBadge')
+ }
+
+ setTxStatusConfirmed (txId) {
+ this._setTxStatus(txId, 'confirmed')
+ }
+
+ // merges txParams obj onto txData.txParams
+ // use extend to ensure that all fields are filled
+ updateTxParams (txId, txParams) {
+ var txMeta = this.getTx(txId)
+ txMeta.txParams = extend(txMeta.txParams, txParams)
+ this.updateTx(txMeta)
+ }
+
+ // checks if a signed tx is in a block and
+ // if included sets the tx status as 'confirmed'
+ checkForTxInBlock () {
+ var signedTxList = this.getFilteredTxList({status: 'signed', err: undefined})
+ if (!signedTxList.length) return
+ signedTxList.forEach((tx) => {
+ var txHash = tx.hash
+ var txId = tx.id
+ if (!txHash) return
+ this.txProviderUtils.query.getTransactionByHash(txHash, (err, txMeta) => {
+ if (err || !txMeta) {
+ tx.err = err || 'Tx could possibly have not been submitted'
+ this.updateTx(tx)
+ return txMeta ? console.error(err) : console.debug(`txMeta is ${txMeta} for:`, tx)
+ }
+ if (txMeta.blockNumber) {
+ this.setTxStatusConfirmed(txId)
+ }
+ })
+ })
+ }
+
+ // 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!
+ // - `'signed'` the tx is signed
+ // - `'submitted'` the tx is sent to a server
+ // - `'confirmed'` the tx has been included in a block.
+ _setTxStatus (txId, status) {
+ var txMeta = this.getTx(txId)
+ txMeta.status = status
+ this.emit(`${txMeta.id}:${status}`, txId)
+ this.updateTx(txMeta)
+ }
+
+ // Saves the new/updated txList.
+ // Function is intended only for internal use
+ _saveTxList (txList) {
+ this.txList = txList
+ this._setTxList(txList)
+ }
+}
+
+
+const warn = () => console.warn('warn was used no cb provided')