Resolve merge conflict from master

feature/default_network_editable
Kevin Serrano 7 years ago
commit e647337a8a
No known key found for this signature in database
GPG Key ID: BF999DEFC7371BA1
  1. 40
      CHANGELOG.md
  2. 2
      app/manifest.json
  3. 48
      app/scripts/controllers/transactions.js
  4. 59
      app/scripts/lib/auto-reload.js
  5. 6
      app/scripts/lib/inpage-provider.js
  6. 107
      app/scripts/lib/nonce-tracker.js
  7. 8
      app/scripts/lib/pending-tx-tracker.js
  8. 37
      app/scripts/lib/tx-state-history-helper.js
  9. 52
      app/scripts/migrations/018.js
  10. 83
      app/scripts/migrations/019.js
  11. 2
      app/scripts/migrations/index.js
  12. 4
      mascara/src/lib/setup-iframe.js
  13. 4
      mascara/src/proxy.js
  14. 7
      package.json
  15. 3053
      test/data/v17-long-history.json
  16. 40
      test/lib/mock-tx-gen.js
  17. 87
      test/unit/actions/tx_test.js
  18. 207
      test/unit/nonce-tracker-test.js
  19. 35
      test/unit/tx-controller-test.js
  20. 26
      test/unit/tx-state-history-helper-test.js
  21. 23
      test/unit/tx-state-history-helper.js
  22. 13
      ui/app/account-detail.js
  23. 23
      ui/app/actions.js
  24. 2
      ui/app/app.js
  25. 11
      ui/app/components/account-dropdowns.js
  26. 2
      ui/app/components/pending-msg-details.js
  27. 5
      ui/app/components/pending-msg.js
  28. 47
      ui/app/components/token-list.js
  29. 11
      ui/app/components/transaction-list-item.js
  30. 2
      ui/app/info.js
  31. 5
      ui/app/reducers.js
  32. 10
      ui/lib/account-link.js

@ -2,12 +2,48 @@
## Current Master
- Make eth_sign deprecation warning less noisy
- Fix bug with network version serialization over synchronous RPC
- Add MetaMask version to state logs.
- Add the total amount of tokens when multiple tokens are added under the token list
- Use HTTPS links for Etherscan.
- Update Support center link to new one with HTTPS.
## 3.9.11 2017-8-24
- Fix nonce calculation bug that would sometimes generate very wrong nonces.
- Give up resubmitting a transaction after 3500 blocks.
## 3.9.10 2017-8-23
- Improve nonce calculation, to prevent bug where people are unable to send transactions reliably.
- Remove link to eth-tx-viz from identicons in tx history.
## 3.9.9 2017-8-18
- Fix bug where some transaction submission errors would show an empty screen.
- Fix bug that could mis-render token balances when very small.
- Fix formatting of eth_sign "Sign Message" view.
- Add deprecation warning to eth_sign "Sign Message" view.
## 3.9.8 2017-8-16
- Reenable token list.
- Remove default tokens.
## 3.9.7 2017-8-15
- hotfix - disable token list
- Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0
## 3.9.6 2017-8-09
- Replace account screen with an account drop-down menu.
- Replace confusing buttons with a new account-specific drop-down menu.
- Replace account buttons with a new account-specific drop-down menu.
## 3.9.5 2017-8-04
- Improved phishing detection configuration update rate
- Improved phishing detection configuration update rate
## 3.9.4 2017-8-03

@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "3.9.5",
"version": "3.9.11",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",

@ -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 })
}
}
}

@ -2,30 +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: (_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/ethereum/mist/releases/tag/v0.9.0')
hasBeenWarned = true
}
// get the time of use
if (name !== '_used') _web3._used = Date.now()
return _web3[name]
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)
}
})
}

@ -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()
@ -80,7 +80,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 +90,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

@ -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

@ -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()
}
}
}

@ -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
}

@ -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
}

@ -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
}

@ -28,4 +28,6 @@ module.exports = [
require('./015'),
require('./016'),
require('./017'),
require('./018'),
require('./019'),
]

@ -1,5 +1,5 @@
const Iframe = require('iframe')
const IframeStream = require('iframe-stream').IframeStream
const createIframeStream = require('iframe-stream').IframeStream
module.exports = setupIframe
@ -13,7 +13,7 @@ function setupIframe(opts) {
})
var iframe = frame.iframe
iframe.style.setProperty('display', 'none')
var iframeStream = new IframeStream(iframe)
var iframeStream = createIframeStream(iframe)
return iframeStream
}

@ -1,4 +1,4 @@
const ParentStream = require('iframe-stream').ParentStream
const createParentStream = require('iframe-stream').ParentStream
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
const SwStream = require('sw-stream/lib/sw-stream.js')
const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js')
@ -11,7 +11,7 @@ const background = new SWcontroller({
intervalDelay,
})
const pageStream = new ParentStream()
const pageStream = createParentStream()
background.on('ready', (_) => {
let swStream = SwStream({
serviceWorker: background.controller,

@ -73,15 +73,16 @@
"eth-query": "^2.1.2",
"eth-sig-util": "^1.2.2",
"eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.1.2",
"eth-token-tracker": "^1.1.3",
"ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0",
"ethjs-ens": "^2.0.0",
"ethjs-query": "^0.2.6",
"ethjs-query": "^0.2.9",
"express": "^4.14.0",
"extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-eslint": "^4.0.0",
@ -185,7 +186,7 @@
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0",
"sinon": "^2.3.8",
"sinon": "^3.2.0",
"tape": "^4.5.1",
"testem": "^1.10.3",
"uglifyify": "^4.0.2",

File diff suppressed because it is too large Load Diff

@ -0,0 +1,40 @@
const extend = require('xtend')
const BN = require('ethereumjs-util').BN
const template = {
'status': 'submitted',
'txParams': {
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
'gas': '0x30d40',
'value': '0x0',
'nonce': '0x3',
},
}
class TxGenerator {
constructor () {
this.txs = []
}
generate (tx = {}, opts = {}) {
let { count, fromNonce } = opts
let nonce = fromNonce || this.txs.length
let txs = []
for (let i = 0; i < count; i++) {
txs.push(extend(template, {
txParams: {
nonce: hexify(nonce++),
}
}, tx))
}
this.txs = this.txs.concat(txs)
return txs
}
}
function hexify (number) {
return '0x' + (new BN(number)).toString(16)
}
module.exports = TxGenerator

@ -53,7 +53,7 @@ describe('tx confirmation screen', function () {
result = reducers(initialState, action)
done()
})
})
it('should transition to the account detail view', function () {
@ -65,91 +65,6 @@ describe('tx confirmation screen', function () {
assert.equal(count, 0)
})
})
describe('sendTx', function () {
var result
describe('when there is an error', function () {
before(function (done) {
actions._setBackgroundConnection({
approveTransaction (txId, cb) { cb({message: 'An error!'}) },
})
actions.sendTx({id: firstTxId})(function (action) {
result = reducers(initialState, action)
done()
})
})
it('should stay on the page', function () {
assert.equal(result.appState.currentView.name, 'confTx')
})
it('should set errorMessage on the currentView', function () {
assert(result.appState.currentView.errorMessage)
})
})
describe('when there is success', function () {
it('should complete tx and go home', function () {
actions._setBackgroundConnection({
approveTransaction (txId, cb) { cb() },
})
var dispatchExpect = sinon.mock()
dispatchExpect.twice()
actions.sendTx({id: firstTxId})(dispatchExpect)
})
})
})
describe('when there are two pending txs', function () {
var firstTxId = 1457634084250832
var result, initialState
before(function (done) {
initialState = {
appState: {
currentView: {
name: 'confTx',
},
},
metamask: {
unapprovedTxs: {
'1457634084250832': {
id: firstTxId,
status: 'unconfirmed',
time: 1457634084250,
},
'1457634084250833': {
id: 1457634084250833,
status: 'unconfirmed',
time: 1457634084255,
},
},
},
}
freeze(initialState)
// Mocking a background connection:
actions._setBackgroundConnection({
approveTransaction (firstTxId, cb) { cb() },
})
actions.sendTx({id: firstTxId})(function (action) {
result = reducers(initialState, action)
})
done()
})
it('should stay on the confTx view', function () {
assert.equal(result.appState.currentView.name, 'confTx')
})
it('should transition to the first tx', function () {
assert.equal(result.appState.currentView.context, 0)
})
})
})
})

@ -1,41 +1,184 @@
const assert = require('assert')
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
let providerResultStub = {}
describe('Nonce Tracker', function () {
let nonceTracker, provider, getPendingTransactions, pendingTxs
beforeEach(function () {
pendingTxs = [{
'status': 'submitted',
'txParams': {
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
'gas': '0x30d40',
'value': '0x0',
'nonce': '0x0',
},
}]
getPendingTransactions = () => pendingTxs
provider = {
sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) },
_blockTracker: {
getCurrentBlock: () => '0x11b568',
},
}
nonceTracker = new NonceTracker({
provider,
getPendingTransactions,
})
})
let nonceTracker, provider
let getPendingTransactions, pendingTxs
let getConfirmedTransactions, confirmedTxs
describe('#getNonceLock', function () {
it('should work', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1')
await nonceLock.releaseLock()
describe('with 3 confirmed and 1 pending', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
})
it('should return 4', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
await nonceLock.releaseLock()
})
})
describe('with no previous txs', function () {
beforeEach(function () {
nonceTracker = generateNonceTrackerWith([], [])
})
it('should return 0', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('with multiple previous txs with same nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 })
pendingTxs = txGen.generate({
status: 'submitted',
txParams: { nonce: '0x01' },
}, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local confirmed count is higher than network nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local pending count is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [])
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when provider nonce is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are some pending nonces below the remote one and some over.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are pending nonces non sequentially over the network nonce.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
txGen.generate({ status: 'submitted' }, { count: 5 })
// 5 over that number
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('When all three return different values', function () {
beforeEach(function () {
const txGen = new MockTxGen()
const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 })
const pendingTxs = txGen.generate({
status: 'submitted',
nonce: 100,
}, { count: 1 })
// 0x32 is 50 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
})
})
function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
const getPendingTransactions = () => pending
const getConfirmedTransactions = () => confirmed
providerResultStub.result = providerStub
const provider = {
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
_blockTracker: {
getCurrentBlock: () => '0x11b568',
},
}
return new NonceTracker({
provider,
getPendingTransactions,
getConfirmedTransactions,
})
}

@ -6,12 +6,15 @@ const clone = require('clone')
const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions')
const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const noop = () => true
const currentNetworkId = 42
const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
const { createStubedProvider } = require('../stub/provider')
describe('Transaction Controller', function () {
let txController, engine, provider, providerResultStub
@ -47,7 +50,7 @@ describe('Transaction Controller', function () {
metamaskNetworkId: currentNetworkId,
txParams,
}
txController._saveTxList([txMeta])
txController.addTx(txMeta)
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta))
})
@ -279,12 +282,15 @@ describe('Transaction Controller', 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)
txController.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} })
var result = txController.getTx('1')
assert.equal(result.hash, 'foo')
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', function () {
it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01'
const desiredGasPrice = '0x02'
@ -297,13 +303,22 @@ describe('Transaction Controller', function () {
},
}
const updatedMeta = clone(txMeta)
txController.addTx(txMeta)
updatedMeta.txParams.gasPrice = desiredGasPrice
txController.updateTx(updatedMeta)
var result = txController.getTx('1')
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)')
})
})

@ -0,0 +1,26 @@
const assert = require('assert')
const clone = require('clone')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
describe('deepCloneFromTxMeta', function () {
it('should clone deep', function () {
const input = {
foo: {
bar: {
bam: 'baz'
}
}
}
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
assert('foo' in output, 'has a foo key')
assert('bar' in output.foo, 'has a bar key')
assert('bam' in output.foo.bar, 'has a bar key')
assert.equal(output.foo.bar.bam, 'baz', 'has a baz value')
})
it('should remove the history key', function () {
const input = { foo: 'bar', history: 'remembered' }
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
assert(typeof output.history, 'undefined', 'should remove history')
})
})

@ -0,0 +1,23 @@
const assert = require('assert')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json')
describe('tx-state-history-helper', function () {
it('migrates history to diffs and can recover original values', function () {
testVault.data.TransactionController.transactions.forEach((tx, index) => {
const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history)
newHistory.forEach((newEntry, index) => {
if (index === 0) {
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj')
} else {
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj')
}
const oldEntry = tx.history[index]
const historySubset = newHistory.slice(0, index + 1)
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset)
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs')
})
})
})
})

@ -107,14 +107,23 @@ AccountDetailScreen.prototype.render = function () {
},
[
h(
'h2.font-medium.color-forest',
'div.font-medium.color-forest',
{
name: 'edit',
style: {
},
},
[
identity && identity.name,
h('h2', {
style: {
maxWidth: '180px',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '5px 0px',
},
}, [
identity && identity.name,
]),
]
),
h(

@ -97,7 +97,6 @@ var actions = {
cancelMsg: cancelMsg,
signPersonalMsg,
cancelPersonalMsg,
sendTx: sendTx,
signTx: signTx,
updateAndApproveTx,
cancelTx: cancelTx,
@ -397,26 +396,13 @@ function signPersonalMsg (msgData) {
function signTx (txData) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
global.ethQuery.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message))
dispatch(actions.hideWarning())
})
dispatch(this.showConfTxPage())
}
}
function sendTx (txData) {
log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`)
return (dispatch) => {
log.debug(`actions calling background.approveTransaction`)
background.approveTransaction(txData.id, (err) => {
if (err) {
dispatch(actions.txError(err))
return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))
if (err) dispatch(actions.displayWarning(err.message))
dispatch(this.goHome())
})
dispatch(actions.showConfTxPage())
}
}
@ -428,6 +414,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.txError(err))
dispatch(actions.goHome())
return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))

@ -187,7 +187,7 @@ App.prototype.renderAppBar = function () {
style: {},
enableAccountsSelector: true,
identities: this.props.identities,
selected: this.props.selected,
selected: this.props.currentView.context,
network: this.props.network,
keyrings: this.props.keyrings,
}, []),

@ -59,7 +59,16 @@ class AccountDropdowns extends Component {
},
),
this.indicateIfLoose(keyring.type),
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, identity.name || ''),
h('span', {
style: {
marginLeft: '20px',
fontSize: '24px',
maxWidth: '145px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}, identity.name || ''),
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
]
)

@ -38,7 +38,7 @@ PendingMsgDetails.prototype.render = function () {
// message data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('.flex-column.flex-space-between', [
h('label.font-small', 'MESSAGE'),
h('span.font-small', msgParams.data),
]),

@ -18,6 +18,9 @@ PendingMsg.prototype.render = function () {
h('div', {
key: msgData.id,
style: {
maxWidth: '350px',
},
}, [
// header
@ -35,7 +38,7 @@ PendingMsg.prototype.render = function () {
}, `Signing this message can have
dangerous side effects. Only sign messages from
sites you fully trust with your entire account.
This will be fixed in a future version.`),
This dangerous method will be removed in a future version.`),
// message details
h(PendingTxDetails, state),

@ -3,17 +3,6 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenTracker = require('eth-token-tracker')
const TokenCell = require('./token-cell.js')
const normalizeAddress = require('eth-sig-util').normalize
const defaultTokens = []
const contracts = require('eth-contract-metadata')
for (const address in contracts) {
const contract = contracts[address]
if (contract.erc20) {
contract.address = address
defaultTokens.push(contract)
}
}
module.exports = TokenList
@ -38,7 +27,24 @@ TokenList.prototype.render = function () {
if (error) {
log.error(error)
return this.message('There was a problem loading your token balances.')
return h('.hotFix', {
style: {
padding: '80px',
},
}, [
'We had trouble loading your token balances. You can view them ',
h('span.hotFix', {
style: {
color: 'rgba(247, 134, 28, 1)',
cursor: 'pointer',
},
onClick: () => {
global.platform.openWindow({
url: `https://ethplorer.io/address/${userAddress}`,
})
},
}, 'here'),
])
}
const tokenViews = tokens.map((tokenData) => {
@ -89,7 +95,7 @@ TokenList.prototype.renderTokenStatusBar = function () {
let msg
if (tokens.length === 1) {
msg = `You own 1 token`
} else if (tokens.length === 1) {
} else if (tokens.length > 1) {
msg = `You own ${tokens.length} tokens`
} else {
msg = `No tokens found`
@ -153,7 +159,7 @@ TokenList.prototype.createFreshTokenTracker = function () {
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: uniqueMergeTokens(defaultTokens, this.props.tokens),
tokens: this.props.tokens,
pollingInterval: 8000,
})
@ -199,16 +205,3 @@ TokenList.prototype.componentWillUnmount = function () {
this.tracker.stop()
}
function uniqueMergeTokens (tokensA, tokensB) {
const uniqueAddresses = []
const result = []
tokensA.concat(tokensB).forEach((token) => {
const normal = normalizeAddress(token.address)
if (!uniqueAddresses.includes(normal)) {
uniqueAddresses.push(normal)
result.push(token)
}
})
return result
}

@ -60,16 +60,7 @@ TransactionListItem.prototype.render = function () {
}, [
h('.identicon-wrapper.flex-column.flex-center.select-none', [
h('.pop-hover', {
onClick: (event) => {
event.stopPropagation()
if (!isTx || isPending) return
var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}`
global.platform.openWindow({ url })
},
}, [
h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
]),
h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
]),
h(Tooltip, {

@ -103,7 +103,7 @@ InfoScreen.prototype.render = function () {
[
h('div.fa.fa-support', [
h('a.info', {
href: 'http://metamask.consensyssupport.happyfox.com',
href: 'https://support.metamask.com',
target: '_blank',
}, 'Visit our Support Center'),
]),

@ -42,7 +42,10 @@ function rootReducer (state, action) {
}
window.logState = function () {
var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2)
let state = window.METAMASK_CACHED_LOG_STATE
const version = global.platform.getVersion()
state.version = version
let stateString = JSON.stringify(state, removeSeedWords, 2)
return stateString
}

@ -3,19 +3,19 @@ module.exports = function (address, network) {
let link
switch (net) {
case 1: // main net
link = `http://etherscan.io/address/${address}`
link = `https://etherscan.io/address/${address}`
break
case 2: // morden test net
link = `http://morden.etherscan.io/address/${address}`
link = `https://morden.etherscan.io/address/${address}`
break
case 3: // ropsten test net
link = `http://ropsten.etherscan.io/address/${address}`
link = `https://ropsten.etherscan.io/address/${address}`
break
case 4: // rinkeby test net
link = `http://rinkeby.etherscan.io/address/${address}`
link = `https://rinkeby.etherscan.io/address/${address}`
break
case 42: // kovan test net
link = `http://kovan.etherscan.io/address/${address}`
link = `https://kovan.etherscan.io/address/${address}`
break
default:
link = ''

Loading…
Cancel
Save