aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/controllers/transactions.js48
-rw-r--r--app/scripts/keyring-controller.js3
-rw-r--r--app/scripts/lib/auto-reload.js62
-rw-r--r--app/scripts/lib/inpage-provider.js11
-rw-r--r--app/scripts/lib/nonce-tracker.js107
-rw-r--r--app/scripts/lib/pending-tx-tracker.js8
-rw-r--r--app/scripts/lib/tx-state-history-helper.js37
-rw-r--r--app/scripts/migrations/018.js52
-rw-r--r--app/scripts/migrations/019.js83
-rw-r--r--app/scripts/migrations/index.js2
11 files changed, 344 insertions, 71 deletions
diff --git a/app/manifest.json b/app/manifest.json
index f34bdcec3..256737c89 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.9.6",
+ "version": "3.9.12",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index 58c468e22..fb3be6073 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -1,6 +1,5 @@
const EventEmitter = require('events')
const extend = require('xtend')
-const clone = require('clone')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const EthQuery = require('ethjs-query')
@@ -8,6 +7,7 @@ const TxProviderUtil = require('../lib/tx-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker')
+const txStateHistoryHelper = require('../lib/tx-state-history-helper')
module.exports = class TransactionController extends EventEmitter {
constructor (opts) {
@@ -33,6 +33,17 @@ module.exports = class TransactionController extends EventEmitter {
err: undefined,
})
},
+ getConfirmedTransactions: (address) => {
+ return this.getFilteredTxList({
+ from: address,
+ status: 'confirmed',
+ 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)
@@ -128,19 +139,17 @@ module.exports = class TransactionController extends EventEmitter {
updateTx (txMeta) {
// create txMeta snapshot for history
- const txMetaForHistory = clone(txMeta)
- // dont include previous history in this snapshot
- delete txMetaForHistory.history
- // add snapshot to tx history
- if (!txMeta.history) txMeta.history = []
- txMeta.history.push(txMetaForHistory)
-
+ 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)
- if (!txMeta.history) txMeta.history = []
- txMeta.history.push(txMetaForHistory)
-
txList[index] = txMeta
this._saveTxList(txList)
this.emit('update')
@@ -148,16 +157,22 @@ module.exports = class TransactionController extends EventEmitter {
// Adds a tx to the txlist
addTx (txMeta) {
- const txCount = this.getTxCount()
- const network = this.getNetwork()
- const fullTxList = this.getFullTxList()
- const txHistoryLimit = this.txHistoryLimit
+ // initialize history
+ 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)
@@ -206,7 +221,6 @@ module.exports = class TransactionController extends EventEmitter {
status: 'unapproved',
metamaskNetworkId: this.getNetwork(),
txParams: txParams,
- history: [],
}
// add default tx params
await this.addTxDefaults(txMeta)
@@ -441,4 +455,4 @@ module.exports = class TransactionController extends EventEmitter {
})
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index 2edc8060e..fd57fac70 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -171,9 +171,9 @@ class KeyringController extends EventEmitter {
return this.setupAccounts(checkedAccounts)
})
.then(() => this.persistAllKeyrings())
+ .then(() => this._updateMemStoreKeyrings())
.then(() => this.fullUpdate())
.then(() => {
- this._updateMemStoreKeyrings()
return keyring
})
}
@@ -208,6 +208,7 @@ class KeyringController extends EventEmitter {
return selectedKeyring.addAccounts(1)
.then(this.setupAccounts.bind(this))
.then(this.persistAllKeyrings.bind(this))
+ .then(this._updateMemStoreKeyrings.bind(this))
.then(this.fullUpdate.bind(this))
}
diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js
index 6abce73ea..cce31c3d2 100644
--- a/app/scripts/lib/auto-reload.js
+++ b/app/scripts/lib/auto-reload.js
@@ -2,33 +2,55 @@ module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
+ let hasBeenWarned = false
+ let reloadInProgress = false
+ let lastTimeUsed
+ let lastSeenNetwork
+
global.web3 = new Proxy(web3, {
- get: (_web3, name) => {
- // get the time of use
- if (name !== '_used') {
- console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/ethereum/mist/releases/tag/v0.9.0')
- _web3._used = Date.now()
+ get: (_web3, key) => {
+ // show warning once on web3 access
+ if (!hasBeenWarned && key !== 'currentProvider') {
+ console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
+ hasBeenWarned = true
}
- return _web3[name]
+ // get the time of use
+ lastTimeUsed = Date.now()
+ // return value normally
+ return _web3[key]
},
- set: (_web3, name, value) => {
- _web3[name] = value
+ set: (_web3, key, value) => {
+ // set value normally
+ _web3[key] = value
},
})
- var networkVersion
observable.subscribe(function (state) {
- // get the initial network
- const curentNetVersion = state.networkVersion
- if (!networkVersion) networkVersion = curentNetVersion
-
- if (curentNetVersion !== networkVersion && web3._used) {
- const timeSinceUse = Date.now() - web3._used
- // if web3 was recently used then delay the reloading of the page
- timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500)
- // prevent reentry into if statement if state updates again before
- // reload
- networkVersion = curentNetVersion
+ // if reload in progress, no need to check reload logic
+ if (reloadInProgress) return
+
+ const currentNetwork = state.networkVersion
+
+ // set the initial network
+ if (!lastSeenNetwork) {
+ lastSeenNetwork = currentNetwork
+ return
+ }
+
+ // skip reload logic if web3 not used
+ if (!lastTimeUsed) return
+
+ // if network did not change, exit
+ if (currentNetwork === lastSeenNetwork) return
+
+ // initiate page reload
+ reloadInProgress = true
+ const timeSinceUse = Date.now() - lastTimeUsed
+ // if web3 was recently used then delay the reloading of the page
+ if (timeSinceUse > 500) {
+ triggerReset()
+ } else {
+ setTimeout(triggerReset, 500)
}
})
}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index fd032a673..c63af06dc 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -27,7 +27,7 @@ function MetamaskInpageProvider (connectionStream) {
)
// ignore phishing warning message (handled elsewhere)
- multiStream.ignoreStream('phishing')
+ multiStream.ignoreStream('phishing')
// connect to async provider
const asyncProvider = self.asyncProvider = new StreamProvider()
@@ -43,8 +43,9 @@ function MetamaskInpageProvider (connectionStream) {
// handle sendAsync requests via asyncProvider
self.sendAsync = function (payload, cb) {
// rewrite request ids
- var request = eachJsonMessage(payload, (message) => {
- var newId = createRandomId()
+ var request = eachJsonMessage(payload, (_message) => {
+ const message = Object.assign({}, _message)
+ const newId = createRandomId()
self.idMap[newId] = message.id
message.id = newId
return message
@@ -80,7 +81,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'eth_coinbase':
// read from localStorage
selectedAddress = self.publicConfigStore.getState().selectedAddress
- result = selectedAddress
+ result = selectedAddress || null
break
case 'eth_uninstallFilter':
@@ -90,7 +91,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'net_version':
const networkVersion = self.publicConfigStore.getState().networkVersion
- result = networkVersion
+ result = networkVersion || null
break
// throw not-supported Error
diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js
index 8328e81ec..0029ac953 100644
--- a/app/scripts/lib/nonce-tracker.js
+++ b/app/scripts/lib/nonce-tracker.js
@@ -1,13 +1,14 @@
-const EthQuery = require('eth-query')
+const EthQuery = require('ethjs-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
class NonceTracker {
- constructor ({ provider, getPendingTransactions }) {
+ constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
+ this.getConfirmedTransactions = getConfirmedTransactions
this.lockMap = {}
}
@@ -25,21 +26,28 @@ class NonceTracker {
await this._globalMutexFree()
// await lock free, then take lock
const releaseLock = await this._takeMutex(address)
- // calculate next nonce
- // we need to make sure our base count
- // and pending count are from the same block
- const currentBlock = await this._getCurrentBlock()
- const pendingTransactions = this.getPendingTransactions(address)
- const pendingCount = pendingTransactions.length
- assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`)
- const baseCountHex = await this._getTxCount(address, currentBlock)
- const baseCount = parseInt(baseCountHex, 16)
- assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
- const nextNonce = baseCount + pendingCount
+ // evaluate multiple nextNonce strategies
+ const nonceDetails = {}
+ const networkNonceResult = await this._getNetworkNextNonce(address)
+ const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
+ const nextNetworkNonce = networkNonceResult.nonce
+ const highestLocalNonce = highestLocallyConfirmed
+ const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce)
+
+ const pendingTxs = this.getPendingTransactions(address)
+ const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
+
+ nonceDetails.params = {
+ highestLocalNonce,
+ highestSuggested,
+ nextNetworkNonce,
+ }
+ nonceDetails.local = localNonceResult
+ nonceDetails.network = networkNonceResult
+
+ const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
- // collect the numbers used to calculate the nonce for debugging
- const blockNumber = currentBlock.number
- const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount }
+
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
}
@@ -53,15 +61,6 @@ class NonceTracker {
})
}
- async _getTxCount (address, currentBlock) {
- const blockNumber = currentBlock.number
- return new Promise((resolve, reject) => {
- this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => {
- err ? reject(err) : resolve(result)
- })
- })
- }
-
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const release = await globalMutex.acquire()
@@ -83,12 +82,68 @@ class NonceTracker {
return mutex
}
+ async _getNetworkNextNonce (address) {
+ // calculate next nonce
+ // we need to make sure our base count
+ // and pending count are from the same block
+ const currentBlock = await this._getCurrentBlock()
+ const blockNumber = currentBlock.blockNumber
+ const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
+ const baseCount = baseCountBN.toNumber()
+ assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
+ const nonceDetails = { blockNumber, baseCount }
+ return { name: 'network', nonce: baseCount, details: nonceDetails }
+ }
+
+ _getHighestLocallyConfirmed (address) {
+ const confirmedTransactions = this.getConfirmedTransactions(address)
+ const highest = this._getHighestNonce(confirmedTransactions)
+ return Number.isInteger(highest) ? highest + 1 : 0
+ }
+
+ _reduceTxListToUniqueNonces (txList) {
+ const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
+ if (!index) return [txMeta]
+ const nonceMatches = txList.filter((txData) => {
+ return txMeta.txParams.nonce === txData.txParams.nonce
+ })
+ if (nonceMatches.length > 1) return reducedList
+ reducedList.push(txMeta)
+ return reducedList
+ }, [])
+ return reducedTxList
+ }
+
+ _getHighestNonce (txList) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ assert(typeof nonce, 'string', 'nonces should be hex strings')
+ return parseInt(nonce, 16)
+ })
+ const highestNonce = Math.max.apply(null, nonces)
+ return highestNonce
+ }
+
+ _getHighestContinuousFrom (txList, startPoint) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ assert(typeof nonce, 'string', 'nonces should be hex strings')
+ return parseInt(nonce, 16)
+ })
+
+ let highest = startPoint
+ while (nonces.includes(highest)) {
+ highest++
+ }
+
+ return { name: 'local', nonce: highest, details: { startPoint, highest } }
+ }
+
// this is a hotfix for the fact that the blockTracker will
// change when the network changes
_getBlockTracker () {
return this.provider._blockTracker
}
-
}
module.exports = NonceTracker
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index 19720db3f..b90851b58 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -1,6 +1,7 @@
const EventEmitter = require('events')
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
@@ -28,6 +29,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.getBalance = config.getBalance
this.getPendingTransactions = config.getPendingTransactions
this.publishTransaction = config.publishTransaction
+ this.giveUpOnTransaction = config.giveUpOnTransaction
}
// checks if a signed tx is in a block and
@@ -100,6 +102,10 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (balance === undefined) return
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.')
@@ -160,4 +166,4 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
nonceGlobalLock.releaseLock()
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js
new file mode 100644
index 000000000..304069d57
--- /dev/null
+++ b/app/scripts/lib/tx-state-history-helper.js
@@ -0,0 +1,37 @@
+const jsonDiffer = require('fast-json-patch')
+const clone = require('clone')
+
+module.exports = {
+ generateHistoryEntry,
+ replayHistory,
+ snapshotFromTxMeta,
+ migrateFromSnapshotsToDiffs,
+}
+
+
+function migrateFromSnapshotsToDiffs(longHistory) {
+ return (
+ longHistory
+ // convert non-initial history entries into diffs
+ .map((entry, index) => {
+ if (index === 0) return entry
+ return generateHistoryEntry(longHistory[index - 1], entry)
+ })
+ )
+}
+
+function generateHistoryEntry(previousState, newState) {
+ return jsonDiffer.compare(previousState, newState)
+}
+
+function replayHistory(shortHistory) {
+ return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
+}
+
+function snapshotFromTxMeta(txMeta) {
+ // create txMeta snapshot for history
+ const snapshot = clone(txMeta)
+ // dont include previous history in this snapshot
+ delete snapshot.history
+ return snapshot
+} \ No newline at end of file
diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js
new file mode 100644
index 000000000..d27fe3f46
--- /dev/null
+++ b/app/scripts/migrations/018.js
@@ -0,0 +1,52 @@
+const version = 18
+
+/*
+
+This migration updates "transaction state history" to diffs style
+
+*/
+
+const clone = require('clone')
+const txStateHistoryHelper = require('../lib/tx-state-history-helper')
+
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ const versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = state
+ const transactions = newState.TransactionController.transactions
+ newState.TransactionController.transactions = transactions.map((txMeta) => {
+ // no history: initialize
+ if (!txMeta.history || txMeta.history.length === 0) {
+ const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
+ txMeta.history = [snapshot]
+ return txMeta
+ }
+ // has history: migrate
+ const newHistory = (
+ txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history)
+ // remove empty diffs
+ .filter((entry) => {
+ return !Array.isArray(entry) || entry.length > 0
+ })
+ )
+ txMeta.history = newHistory
+ return txMeta
+ })
+ return newState
+}
diff --git a/app/scripts/migrations/019.js b/app/scripts/migrations/019.js
new file mode 100644
index 000000000..072c96370
--- /dev/null
+++ b/app/scripts/migrations/019.js
@@ -0,0 +1,83 @@
+
+const version = 19
+
+/*
+
+This migration sets transactions as failed
+whos nonce is too high
+
+*/
+
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ const versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = state
+ const transactions = newState.TransactionController.transactions
+
+ newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
+ if (txMeta.status !== 'submitted') return txMeta
+
+ const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed')
+ .filter((tx) => tx.txParams.from === txMeta.txParams.from)
+ .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
+ const highestConfirmedNonce = getHighestNonce(confirmedTxs)
+
+ const pendingTxs = txList.filter((tx) => tx.status === 'submitted')
+ .filter((tx) => tx.txParams.from === txMeta.txParams.from)
+ .filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
+ const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce)
+
+ const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce)
+
+ if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) {
+ txMeta.status = 'failed'
+ txMeta.err = {
+ message: 'nonce too high',
+ note: 'migration 019 custom error',
+ }
+ }
+ return txMeta
+ })
+ return newState
+}
+
+function getHighestContinuousFrom (txList, startPoint) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ return parseInt(nonce, 16)
+ })
+
+ let highest = startPoint
+ while (nonces.includes(highest)) {
+ highest++
+ }
+
+ return highest
+}
+
+function getHighestNonce (txList) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ return parseInt(nonce || '0x0', 16)
+ })
+ const highestNonce = Math.max.apply(null, nonces)
+ return highestNonce
+}
+
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index f4c87499f..e9cbd7b98 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -28,4 +28,6 @@ module.exports = [
require('./015'),
require('./016'),
require('./017'),
+ require('./018'),
+ require('./019'),
]