Merge pull request #11120 from MetaMask/Version-v9.5.3
Version v9.5.3 RCfeature/default_network_editable
commit
38e8cc8303
@ -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); |
||||
}); |
||||
}); |
@ -0,0 +1,73 @@ |
||||
import { |
||||
isHexString, |
||||
isValidAddress, |
||||
isValidChecksumAddress, |
||||
addHexPrefix, |
||||
toChecksumAddress, |
||||
} from 'ethereumjs-util'; |
||||
|
||||
export const BURN_ADDRESS = '0x0000000000000000000000000000000000000000'; |
||||
|
||||
export function isBurnAddress(address) { |
||||
return address === BURN_ADDRESS; |
||||
} |
||||
|
||||
/** |
||||
* Validates that the input is a hex address. This utility method is a thin |
||||
* wrapper around ethereumjs-util.isValidAddress, with the exception that it |
||||
* does not throw an error when provided values that are not hex strings. In |
||||
* addition, and by default, this method will return true for hex strings that |
||||
* meet the length requirement of a hex address, but are not prefixed with `0x` |
||||
* Finally, if the mixedCaseUseChecksum flag is true and a mixed case string is |
||||
* provided this method will validate it has the proper checksum formatting. |
||||
* @param {string} possibleAddress - Input parameter to check against |
||||
* @param {Object} [options] - options bag |
||||
* @param {boolean} [options.allowNonPrefixed] - If true will first ensure '0x' |
||||
* is prepended to the string |
||||
* @param {boolean} [options.mixedCaseUseChecksum] - If true will treat mixed |
||||
* case addresses as checksum addresses and validate that proper checksum |
||||
* format is used |
||||
* @returns {boolean} whether or not the input is a valid hex address |
||||
*/ |
||||
export function isValidHexAddress( |
||||
possibleAddress, |
||||
{ allowNonPrefixed = true, mixedCaseUseChecksum = false } = {}, |
||||
) { |
||||
const addressToCheck = allowNonPrefixed |
||||
? addHexPrefix(possibleAddress) |
||||
: possibleAddress; |
||||
if (!isHexString(addressToCheck)) { |
||||
return false; |
||||
} |
||||
|
||||
if (mixedCaseUseChecksum) { |
||||
const prefixRemoved = addressToCheck.slice(2); |
||||
const lower = prefixRemoved.toLowerCase(); |
||||
const upper = prefixRemoved.toUpperCase(); |
||||
const allOneCase = prefixRemoved === lower || prefixRemoved === upper; |
||||
if (!allOneCase) { |
||||
return isValidChecksumAddress(addressToCheck); |
||||
} |
||||
} |
||||
|
||||
return isValidAddress(addressToCheck); |
||||
} |
||||
|
||||
export function toChecksumHexAddress(address) { |
||||
if (!address) { |
||||
// our internal checksumAddress function that this method replaces would
|
||||
// return an empty string for nullish input. If any direct usages of
|
||||
// ethereumjs-util.toChecksumAddress were called with nullish input it
|
||||
// would have resulted in an error on version 5.1.
|
||||
return ''; |
||||
} |
||||
const hexPrefixed = addHexPrefix(address); |
||||
if (!isHexString(hexPrefixed)) { |
||||
// Version 5.1 of ethereumjs-utils would have returned '0xY' for input 'y'
|
||||
// but we shouldn't waste effort trying to change case on a clearly invalid
|
||||
// string. Instead just return the hex prefixed original string which most
|
||||
// closely mimics the original behavior.
|
||||
return hexPrefixed; |
||||
} |
||||
return toChecksumAddress(addHexPrefix(address)); |
||||
} |
@ -0,0 +1,57 @@ |
||||
import { strict as assert } from 'assert'; |
||||
import { toChecksumAddress } from 'ethereumjs-util'; |
||||
import { isValidHexAddress } from './hexstring-utils'; |
||||
|
||||
describe('hexstring utils', function () { |
||||
describe('isValidHexAddress', function () { |
||||
it('should allow 40-char non-prefixed hex', function () { |
||||
const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825'; |
||||
const result = isValidHexAddress(address); |
||||
assert.equal(result, true); |
||||
}); |
||||
|
||||
it('should allow 42-char prefixed hex', function () { |
||||
const address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'; |
||||
const result = isValidHexAddress(address); |
||||
assert.equal(result, true); |
||||
}); |
||||
|
||||
it('should NOT allow 40-char non-prefixed hex when allowNonPrefixed is false', function () { |
||||
const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825'; |
||||
const result = isValidHexAddress(address, { allowNonPrefixed: false }); |
||||
assert.equal(result, false); |
||||
}); |
||||
|
||||
it('should NOT allow any length of non hex-prefixed string', function () { |
||||
const address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b85'; |
||||
const result = isValidHexAddress(address); |
||||
assert.equal(result, false); |
||||
}); |
||||
|
||||
it('should NOT allow less than 42 character hex-prefixed string', function () { |
||||
const address = '0xfdea65ce26263f6d9a1b5de9555d2931a33b85'; |
||||
const result = isValidHexAddress(address); |
||||
assert.equal(result, false); |
||||
}); |
||||
|
||||
it('should recognize correct capitalized checksum', function () { |
||||
const address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A33b825'; |
||||
const result = isValidHexAddress(address, { mixedCaseUseChecksum: true }); |
||||
assert.equal(result, true); |
||||
}); |
||||
|
||||
it('should recognize incorrect capitalized checksum', function () { |
||||
const address = '0xFDea65C8e26263F6d9A1B5de9555D2931A33b825'; |
||||
const result = isValidHexAddress(address, { mixedCaseUseChecksum: true }); |
||||
assert.equal(result, false); |
||||
}); |
||||
|
||||
it('should recognize this sample hashed address', function () { |
||||
const address = '0x5Fda30Bb72B8Dfe20e48A00dFc108d0915BE9Bb0'; |
||||
const result = isValidHexAddress(address, { mixedCaseUseChecksum: true }); |
||||
const hashed = toChecksumAddress(address.toLowerCase()); |
||||
assert.equal(hashed, address); |
||||
assert.equal(result, true); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,8 @@ |
||||
.transaction-icon { |
||||
&__grey-circle { |
||||
height: 28px; |
||||
width: 28px; |
||||
border-radius: 14px; |
||||
background: $Grey-100; |
||||
} |
||||
} |
Loading…
Reference in new issue