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