mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 01:39:44 +01:00
Merge pull request #1914 from MetaMask/history-diff
Move Tx State History to diff-based format
This commit is contained in:
commit
e0c35179c2
@ -1,6 +1,5 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const extend = require('xtend')
|
const extend = require('xtend')
|
||||||
const clone = require('clone')
|
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
@ -8,6 +7,7 @@ const TxProviderUtil = require('../lib/tx-utils')
|
|||||||
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
||||||
const createId = require('../lib/random-id')
|
const createId = require('../lib/random-id')
|
||||||
const NonceTracker = require('../lib/nonce-tracker')
|
const NonceTracker = require('../lib/nonce-tracker')
|
||||||
|
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
|
||||||
|
|
||||||
module.exports = class TransactionController extends EventEmitter {
|
module.exports = class TransactionController extends EventEmitter {
|
||||||
constructor (opts) {
|
constructor (opts) {
|
||||||
@ -128,19 +128,17 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
updateTx (txMeta) {
|
updateTx (txMeta) {
|
||||||
// create txMeta snapshot for history
|
// create txMeta snapshot for history
|
||||||
const txMetaForHistory = clone(txMeta)
|
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
// dont include previous history in this snapshot
|
// recover previous tx state obj
|
||||||
delete txMetaForHistory.history
|
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
|
||||||
// add snapshot to tx history
|
// generate history entry and add to history
|
||||||
if (!txMeta.history) txMeta.history = []
|
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState)
|
||||||
txMeta.history.push(txMetaForHistory)
|
txMeta.history.push(entry)
|
||||||
|
|
||||||
|
// commit txMeta to state
|
||||||
const txId = txMeta.id
|
const txId = txMeta.id
|
||||||
const txList = this.getFullTxList()
|
const txList = this.getFullTxList()
|
||||||
const index = txList.findIndex(txData => txData.id === txId)
|
const index = txList.findIndex(txData => txData.id === txId)
|
||||||
if (!txMeta.history) txMeta.history = []
|
|
||||||
txMeta.history.push(txMetaForHistory)
|
|
||||||
|
|
||||||
txList[index] = txMeta
|
txList[index] = txMeta
|
||||||
this._saveTxList(txList)
|
this._saveTxList(txList)
|
||||||
this.emit('update')
|
this.emit('update')
|
||||||
@ -148,16 +146,22 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
// Adds a tx to the txlist
|
// Adds a tx to the txlist
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
const txCount = this.getTxCount()
|
// initialize history
|
||||||
const network = this.getNetwork()
|
txMeta.history = []
|
||||||
const fullTxList = this.getFullTxList()
|
// capture initial snapshot of txMeta for history
|
||||||
const txHistoryLimit = this.txHistoryLimit
|
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
|
txMeta.history.push(snapshot)
|
||||||
|
|
||||||
// checks if the length of the tx history is
|
// checks if the length of the tx history is
|
||||||
// longer then desired persistence limit
|
// longer then desired persistence limit
|
||||||
// and then if it is removes only confirmed
|
// and then if it is removes only confirmed
|
||||||
// or rejected tx's.
|
// or rejected tx's.
|
||||||
// not tx's that are pending or unapproved
|
// 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) {
|
if (txCount > txHistoryLimit - 1) {
|
||||||
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
|
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
|
||||||
fullTxList.splice(index, 1)
|
fullTxList.splice(index, 1)
|
||||||
@ -206,7 +210,6 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
metamaskNetworkId: this.getNetwork(),
|
metamaskNetworkId: this.getNetwork(),
|
||||||
txParams: txParams,
|
txParams: txParams,
|
||||||
history: [],
|
|
||||||
}
|
}
|
||||||
// add default tx params
|
// add default tx params
|
||||||
await this.addTxDefaults(txMeta)
|
await this.addTxDefaults(txMeta)
|
||||||
|
37
app/scripts/lib/tx-state-history-helper.js
Normal file
37
app/scripts/lib/tx-state-history-helper.js
Normal file
@ -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
|
||||||
|
}
|
52
app/scripts/migrations/018.js
Normal file
52
app/scripts/migrations/018.js
Normal file
@ -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
|
||||||
|
}
|
@ -28,4 +28,5 @@ module.exports = [
|
|||||||
require('./015'),
|
require('./015'),
|
||||||
require('./016'),
|
require('./016'),
|
||||||
require('./017'),
|
require('./017'),
|
||||||
|
require('./018'),
|
||||||
]
|
]
|
||||||
|
@ -82,6 +82,7 @@
|
|||||||
"express": "^4.14.0",
|
"express": "^4.14.0",
|
||||||
"extension-link-enabler": "^1.0.0",
|
"extension-link-enabler": "^1.0.0",
|
||||||
"extensionizer": "^1.0.0",
|
"extensionizer": "^1.0.0",
|
||||||
|
"fast-json-patch": "^2.0.4",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
"gulp": "github:gulpjs/gulp#4.0",
|
"gulp": "github:gulpjs/gulp#4.0",
|
||||||
"gulp-eslint": "^4.0.0",
|
"gulp-eslint": "^4.0.0",
|
||||||
|
3053
test/data/v17-long-history.json
Normal file
3053
test/data/v17-long-history.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,12 +6,15 @@ const clone = require('clone')
|
|||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const TransactionController = require('../../app/scripts/controllers/transactions')
|
const TransactionController = require('../../app/scripts/controllers/transactions')
|
||||||
const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
|
const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
|
||||||
|
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
||||||
|
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
const currentNetworkId = 42
|
const currentNetworkId = 42
|
||||||
const otherNetworkId = 36
|
const otherNetworkId = 36
|
||||||
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
||||||
const { createStubedProvider } = require('../stub/provider')
|
const { createStubedProvider } = require('../stub/provider')
|
||||||
|
|
||||||
|
|
||||||
describe('Transaction Controller', function () {
|
describe('Transaction Controller', function () {
|
||||||
let txController, engine, provider, providerResultStub
|
let txController, engine, provider, providerResultStub
|
||||||
|
|
||||||
@ -47,7 +50,7 @@ describe('Transaction Controller', function () {
|
|||||||
metamaskNetworkId: currentNetworkId,
|
metamaskNetworkId: currentNetworkId,
|
||||||
txParams,
|
txParams,
|
||||||
}
|
}
|
||||||
txController._saveTxList([txMeta])
|
txController.addTx(txMeta)
|
||||||
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(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 () {
|
it('replaces the tx with the same id', function () {
|
||||||
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
txController.addTx({ id: '2', status: 'confirmed', 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: {} })
|
const tx1 = txController.getTx('1')
|
||||||
var result = txController.getTx('1')
|
tx1.status = 'blah'
|
||||||
assert.equal(result.hash, 'foo')
|
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 originalGasPrice = '0x01'
|
||||||
const desiredGasPrice = '0x02'
|
const desiredGasPrice = '0x02'
|
||||||
|
|
||||||
@ -297,13 +303,22 @@ describe('Transaction Controller', function () {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMeta = clone(txMeta)
|
|
||||||
|
|
||||||
txController.addTx(txMeta)
|
txController.addTx(txMeta)
|
||||||
updatedMeta.txParams.gasPrice = desiredGasPrice
|
const updatedTx = txController.getTx('1')
|
||||||
txController.updateTx(updatedMeta)
|
// verify tx was initialized correctly
|
||||||
var result = txController.getTx('1')
|
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')
|
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)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
23
test/unit/tx-state-history-helper.js
Normal file
23
test/unit/tx-state-history-helper.js
Normal file
@ -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
Block a user