commit
e647337a8a
@ -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 |
||||
} |
||||
|
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 |
@ -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, |
||||
}) |
||||
} |
||||
|
||||
|
@ -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') |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
Loading…
Reference in new issue