Migration to remove erroneous tx state (#11107)
parent
c3b99a5921
commit
001a01e5b2
@ -0,0 +1,52 @@ |
||||
import { |
||||
cloneDeep, |
||||
concat, |
||||
groupBy, |
||||
keyBy, |
||||
pickBy, |
||||
isPlainObject, |
||||
} from 'lodash'; |
||||
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; |
||||
|
||||
const version = 59; |
||||
|
||||
/** |
||||
* Removes orphaned cancel and retry transactions that no longer have the |
||||
* original transaction in state, which results in bugs. |
||||
*/ |
||||
export default { |
||||
version, |
||||
async migrate(originalVersionedData) { |
||||
const versionedData = cloneDeep(originalVersionedData); |
||||
versionedData.meta.version = version; |
||||
const state = versionedData.data; |
||||
versionedData.data = transformState(state); |
||||
return versionedData; |
||||
}, |
||||
}; |
||||
|
||||
function transformState(state) { |
||||
const transactions = state?.TransactionController?.transactions; |
||||
if (isPlainObject(transactions)) { |
||||
const nonceNetworkGroupedObject = groupBy( |
||||
Object.values(transactions), |
||||
(tx) => { |
||||
return `${tx.txParams?.nonce}-${tx.chainId ?? tx.metamaskNetworkId}`; |
||||
}, |
||||
); |
||||
|
||||
const withoutOrphans = pickBy(nonceNetworkGroupedObject, (group) => { |
||||
return group.some( |
||||
(tx) => |
||||
tx.type !== TRANSACTION_TYPES.CANCEL && |
||||
tx.type !== TRANSACTION_TYPES.RETRY, |
||||
); |
||||
}); |
||||
state.TransactionController.transactions = keyBy( |
||||
concat(...Object.values(withoutOrphans)), |
||||
(tx) => tx.id, |
||||
); |
||||
} |
||||
|
||||
return state; |
||||
} |
@ -0,0 +1,385 @@ |
||||
import { strict as assert } from 'assert'; |
||||
import { cloneDeep } from 'lodash'; |
||||
import { |
||||
KOVAN_CHAIN_ID, |
||||
MAINNET_CHAIN_ID, |
||||
RINKEBY_CHAIN_ID, |
||||
GOERLI_CHAIN_ID, |
||||
} from '../../../shared/constants/network'; |
||||
import { |
||||
TRANSACTION_TYPES, |
||||
TRANSACTION_STATUSES, |
||||
} from '../../../shared/constants/transaction'; |
||||
import migration59 from './059'; |
||||
|
||||
const ERRONEOUS_TRANSACTION_STATE = { |
||||
0: { |
||||
type: TRANSACTION_TYPES.CANCEL, |
||||
id: 0, |
||||
chainId: MAINNET_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x0', |
||||
}, |
||||
}, |
||||
1: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 1, |
||||
chainId: MAINNET_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x1', |
||||
}, |
||||
}, |
||||
2: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 2, |
||||
chainId: KOVAN_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x2', |
||||
}, |
||||
}, |
||||
3: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 3, |
||||
chainId: RINKEBY_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x3', |
||||
}, |
||||
}, |
||||
4: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 4, |
||||
chainId: RINKEBY_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x4', |
||||
}, |
||||
}, |
||||
5: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 5, |
||||
chainId: MAINNET_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x5', |
||||
}, |
||||
}, |
||||
6: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 6, |
||||
chainId: KOVAN_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x6', |
||||
}, |
||||
}, |
||||
7: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 7, |
||||
chainId: RINKEBY_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x7', |
||||
}, |
||||
}, |
||||
8: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 8, |
||||
chainId: RINKEBY_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0x8', |
||||
}, |
||||
}, |
||||
9: { |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
id: 9, |
||||
chainId: RINKEBY_CHAIN_ID, |
||||
status: TRANSACTION_STATUSES.UNAPPROVED, |
||||
}, |
||||
}; |
||||
|
||||
const ERRONEOUS_TRANSACTION_STATE_RETRY = { |
||||
...ERRONEOUS_TRANSACTION_STATE, |
||||
0: { |
||||
...ERRONEOUS_TRANSACTION_STATE[0], |
||||
type: TRANSACTION_TYPES.RETRY, |
||||
}, |
||||
}; |
||||
|
||||
const ERRONEOUS_TRANSACTION_STATE_MIXED = { |
||||
...ERRONEOUS_TRANSACTION_STATE, |
||||
10: { |
||||
type: TRANSACTION_TYPES.RETRY, |
||||
id: 10, |
||||
chainId: MAINNET_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0xa', |
||||
}, |
||||
}, |
||||
11: { |
||||
type: TRANSACTION_TYPES.RETRY, |
||||
id: 11, |
||||
chainId: MAINNET_CHAIN_ID, |
||||
txParams: { |
||||
nonce: '0xb', |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
describe('migration #59', function () { |
||||
it('should update the version metadata', async function () { |
||||
const oldStorage = { |
||||
meta: { |
||||
version: 58, |
||||
}, |
||||
data: {}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(newStorage.meta, { |
||||
version: 59, |
||||
}); |
||||
}); |
||||
|
||||
it('should drop orphaned cancel transactions', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: ERRONEOUS_TRANSACTION_STATE, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
const EXPECTED = cloneDeep(ERRONEOUS_TRANSACTION_STATE); |
||||
delete EXPECTED['0']; |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: EXPECTED, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should drop orphaned cancel transactions even if a nonce exists on another network that is confirmed', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: { |
||||
...ERRONEOUS_TRANSACTION_STATE, |
||||
11: { |
||||
...ERRONEOUS_TRANSACTION_STATE['0'], |
||||
id: 11, |
||||
chainId: GOERLI_CHAIN_ID, |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
}, |
||||
}, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
const EXPECTED = cloneDeep( |
||||
oldStorage.data.TransactionController.transactions, |
||||
); |
||||
delete EXPECTED['0']; |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: EXPECTED, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should not drop cancel transactions with matching non cancel or retry in same network and nonce', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: { |
||||
...ERRONEOUS_TRANSACTION_STATE, |
||||
11: { |
||||
...ERRONEOUS_TRANSACTION_STATE['0'], |
||||
id: 11, |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
}, |
||||
}, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: oldStorage.data.TransactionController.transactions, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should drop orphaned retry transactions', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: ERRONEOUS_TRANSACTION_STATE_RETRY, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
const EXPECTED = cloneDeep(ERRONEOUS_TRANSACTION_STATE_RETRY); |
||||
delete EXPECTED['0']; |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: EXPECTED, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should drop orphaned retry transactions even if a nonce exists on another network that is confirmed', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: { |
||||
...ERRONEOUS_TRANSACTION_STATE_RETRY, |
||||
11: { |
||||
...ERRONEOUS_TRANSACTION_STATE_RETRY['0'], |
||||
id: 11, |
||||
chainId: GOERLI_CHAIN_ID, |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
}, |
||||
}, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
const EXPECTED = cloneDeep( |
||||
oldStorage.data.TransactionController.transactions, |
||||
); |
||||
delete EXPECTED['0']; |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: EXPECTED, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should not drop retry transactions with matching non cancel or retry in same network and nonce', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: { |
||||
...ERRONEOUS_TRANSACTION_STATE_RETRY, |
||||
11: { |
||||
...ERRONEOUS_TRANSACTION_STATE_RETRY['0'], |
||||
id: 11, |
||||
type: TRANSACTION_TYPES.SENT_ETHER, |
||||
}, |
||||
}, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: oldStorage.data.TransactionController.transactions, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should drop all orphaned retry and cancel transactions', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: ERRONEOUS_TRANSACTION_STATE_MIXED, |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
// The following ERRONEOUS_TRANSACTION_STATE object only has one orphan in it
|
||||
// so using it as the base for our expected output automatically removes a few
|
||||
// transactions we expect to be missing.
|
||||
const EXPECTED = cloneDeep(ERRONEOUS_TRANSACTION_STATE); |
||||
delete EXPECTED['0']; |
||||
assert.deepEqual(newStorage.data, { |
||||
TransactionController: { |
||||
transactions: EXPECTED, |
||||
}, |
||||
foo: 'bar', |
||||
}); |
||||
}); |
||||
|
||||
it('should do nothing if transactions state does not exist', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
bar: 'baz', |
||||
}, |
||||
IncomingTransactionsController: { |
||||
foo: 'baz', |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(oldStorage.data, newStorage.data); |
||||
}); |
||||
|
||||
it('should do nothing if transactions state is empty', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: {}, |
||||
bar: 'baz', |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(oldStorage.data, newStorage.data); |
||||
}); |
||||
|
||||
it('should do nothing if transactions state is not an object', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
TransactionController: { |
||||
transactions: [], |
||||
bar: 'baz', |
||||
}, |
||||
foo: 'bar', |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(oldStorage.data, newStorage.data); |
||||
}); |
||||
|
||||
it('should do nothing if state is empty', async function () { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: {}, |
||||
}; |
||||
|
||||
const newStorage = await migration59.migrate(oldStorage); |
||||
assert.deepEqual(oldStorage.data, newStorage.data); |
||||
}); |
||||
}); |
Loading…
Reference in new issue