1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Fix account selectors when balances are missing (#20385)

* Fix account selectors when balances are missing

Some of the account selectors we use would return an empty set of
accounts if the `AccountTracker` state was missing. This resulted in UI
crashes when trying to access the current selected account.

The selectors have been updated to use the `identities` as the source-
of-truth for the full set of accounts. This ensures that even if the
balances are missing, each account will at least be represented by an
empty object.

* Fix unit test

* Fix another unit test

* Fix another unit test

* Fix another unit test

* Fix more unit tests

---------

Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
This commit is contained in:
Mark Stacey 2023-08-26 00:28:24 -02:30
parent d5de0dd67e
commit b684c094cb
8 changed files with 116 additions and 65 deletions

View File

@ -61,6 +61,9 @@ const render = ({ txProps, contextProps } = {}) => {
balance: '0x1F4', balance: '0x1F4',
}, },
}, },
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress', selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true }, featureFlags: { advancedInlineGas: true },
gasFeeEstimates: MOCK_FEE_ESTIMATE, gasFeeEstimates: MOCK_FEE_ESTIMATE,

View File

@ -67,6 +67,9 @@ const renderComponent = ({
balance: '0x176e5b6f173ebe66', balance: '0x176e5b6f173ebe66',
}, },
}, },
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress', selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true }, featureFlags: { advancedInlineGas: true },
gasEstimateType: 'fee-market', gasEstimateType: 'fee-market',

View File

@ -41,6 +41,9 @@ const renderComponent = (componentProps) => {
balance: '0x176e5b6f173ebe66', balance: '0x176e5b6f173ebe66',
}, },
}, },
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress', selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true }, featureFlags: { advancedInlineGas: true },
}, },

View File

@ -28,6 +28,9 @@ const render = () => {
balance: '0x176e5b6f173ebe66', balance: '0x176e5b6f173ebe66',
}, },
}, },
identities: {
'0xAddress': {},
},
selectedAddress: '0xAddress', selectedAddress: '0xAddress',
}, },
}); });

View File

@ -11,7 +11,7 @@ import withModalProps from '../../../helpers/higher-order-components/with-modal-
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import { mmiActionsFactory } from '../../../store/institutional/institution-background'; import { mmiActionsFactory } from '../../../store/institutional/institution-background';
import { setSelectedAddress } from '../../../store/actions'; import { setSelectedAddress } from '../../../store/actions';
import { getMetaMaskAccountsRaw } from '../../../selectors'; import { getMetaMaskIdentities } from '../../../selectors';
import { import {
getMMIAddressFromModalOrAddress, getMMIAddressFromModalOrAddress,
getCustodyAccountDetails, getCustodyAccountDetails,
@ -42,7 +42,7 @@ const CustodyConfirmLink = ({ hideModal }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const mmiActions = mmiActionsFactory(); const mmiActions = mmiActionsFactory();
const trackEvent = useContext(MetaMetricsContext); const trackEvent = useContext(MetaMetricsContext);
const mmiAccounts = useSelector(getMetaMaskAccountsRaw); const mmiAccounts = useSelector(getMetaMaskIdentities);
const address = useSelector(getMMIAddressFromModalOrAddress); const address = useSelector(getMMIAddressFromModalOrAddress);
const custodyAccountDetails = useSelector(getCustodyAccountDetails); const custodyAccountDetails = useSelector(getCustodyAccountDetails);
const { custodians } = useSelector(getMMIConfiguration); const { custodians } = useSelector(getMMIConfiguration);

View File

@ -77,6 +77,8 @@ import { draftTransactionInitialState, editExistingTransaction } from '.';
const mockStore = createMockStore([thunk]); const mockStore = createMockStore([thunk]);
const mockAddress1 = '0xdafea492d9c6733ae3d56b7ed1adb60692c98123';
jest.mock('./send', () => { jest.mock('./send', () => {
const actual = jest.requireActual('./send'); const actual = jest.requireActual('./send');
return { return {
@ -1063,7 +1065,7 @@ describe('Send Slice', () => {
describe('QR Code Detected', () => { describe('QR Code Detected', () => {
const qrCodestate = getInitialSendStateWithExistingTxState({ const qrCodestate = getInitialSendStateWithExistingTxState({
recipient: { recipient: {
address: '0xAddress', address: mockAddress1,
}, },
}); });
@ -1102,7 +1104,7 @@ describe('Send Slice', () => {
const draftTransaction = getTestUUIDTx(result); const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.recipient.address).toStrictEqual('0xAddress'); expect(draftTransaction.recipient.address).toStrictEqual(mockAddress1);
expect(draftTransaction.recipient.error).toStrictEqual( expect(draftTransaction.recipient.error).toStrictEqual(
INVALID_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR,
); );
@ -1115,7 +1117,7 @@ describe('Send Slice', () => {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
selectedAccount: { selectedAccount: {
balance: '0x0', balance: '0x0',
address: '0xAddress', address: mockAddress1,
}, },
}; };
@ -1144,7 +1146,7 @@ describe('Send Slice', () => {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
selectedAccount: { selectedAccount: {
balance: '0x0', balance: '0x0',
address: '0xAddress', address: mockAddress1,
}, },
}; };
@ -1158,7 +1160,7 @@ describe('Send Slice', () => {
const result = sendReducer(olderState, action); const result = sendReducer(olderState, action);
expect(result.selectedAccount.balance).toStrictEqual('0x0'); expect(result.selectedAccount.balance).toStrictEqual('0x0');
expect(result.selectedAccount.address).toStrictEqual('0xAddress'); expect(result.selectedAccount.address).toStrictEqual(mockAddress1);
}); });
}); });
@ -1167,13 +1169,13 @@ describe('Send Slice', () => {
const accountsChangedState = { const accountsChangedState = {
...getInitialSendStateWithExistingTxState({ ...getInitialSendStateWithExistingTxState({
fromAccount: { fromAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}), }),
stage: SEND_STAGES.EDIT, stage: SEND_STAGES.EDIT,
selectedAccount: { selectedAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}; };
@ -1182,7 +1184,7 @@ describe('Send Slice', () => {
type: 'ACCOUNT_CHANGED', type: 'ACCOUNT_CHANGED',
payload: { payload: {
account: { account: {
address: '0xAddress', address: mockAddress1,
balance: '0x1', balance: '0x1',
}, },
}, },
@ -1201,13 +1203,13 @@ describe('Send Slice', () => {
const accountsChangedState = { const accountsChangedState = {
...getInitialSendStateWithExistingTxState({ ...getInitialSendStateWithExistingTxState({
fromAccount: { fromAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}), }),
stage: SEND_STAGES.EDIT, stage: SEND_STAGES.EDIT,
selectedAccount: { selectedAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}; };
@ -1231,7 +1233,7 @@ describe('Send Slice', () => {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
stage: SEND_STAGES.EDIT, stage: SEND_STAGES.EDIT,
selectedAccount: { selectedAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}; };
@ -1274,23 +1276,23 @@ describe('Send Slice', () => {
1559: true, 1559: true,
}, },
}, },
selectedAddress: '0xAddress', selectedAddress: mockAddress1,
identities: { '0xAddress': { address: '0xAddress' } }, identities: { [mockAddress1]: { address: mockAddress1 } },
keyrings: [ keyrings: [
{ {
type: KeyringType.hdKeyTree, type: KeyringType.hdKeyTree,
accounts: ['0xAddress'], accounts: [mockAddress1],
}, },
], ],
accounts: { accounts: {
'0xAddress': { [mockAddress1]: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}, },
cachedBalances: { cachedBalances: {
0x5: { 0x5: {
'0xAddress': '0x0', [mockAddress1]: '0x0',
}, },
}, },
providerConfig: { providerConfig: {
@ -1580,14 +1582,17 @@ describe('Send Slice', () => {
}, },
cachedBalances: { cachedBalances: {
[CHAIN_IDS.GOERLI]: { [CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0', [mockAddress1]: '0x0',
}, },
}, },
accounts: { accounts: {
'0xAddress': { [mockAddress1]: {
address: '0xAddress', address: mockAddress1,
}, },
}, },
identities: {
[mockAddress1]: {},
},
}, },
send: { send: {
...getInitialSendStateWithExistingTxState({ ...getInitialSendStateWithExistingTxState({
@ -1607,7 +1612,7 @@ describe('Send Slice', () => {
userInputHexData: '', userInputHexData: '',
}), }),
selectedAccount: { selectedAccount: {
address: '0xAddress', address: mockAddress1,
}, },
}, },
}; };
@ -2379,16 +2384,18 @@ describe('Send Slice', () => {
addressBook: { addressBook: {
[CHAIN_IDS.GOERLI]: {}, [CHAIN_IDS.GOERLI]: {},
}, },
identities: {}, identities: {
[mockAddress1]: {},
},
accounts: { accounts: {
'0xAddress': { [mockAddress1]: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}, },
cachedBalances: { cachedBalances: {
[CHAIN_IDS.GOERLI]: { [CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0', [mockAddress1]: '0x0',
}, },
}, },
tokenList: {}, tokenList: {},
@ -2397,7 +2404,7 @@ describe('Send Slice', () => {
id: 1, id: 1,
txParams: { txParams: {
data: '', data: '',
from: '0xAddress', from: mockAddress1,
to: '0xRecipientAddress', to: '0xRecipientAddress',
gas: GAS_LIMITS.SIMPLE, gas: GAS_LIMITS.SIMPLE,
gasPrice: '0x3b9aca00', // 1000000000 gasPrice: '0x3b9aca00', // 1000000000
@ -2414,7 +2421,7 @@ describe('Send Slice', () => {
...getInitialSendStateWithExistingTxState({ ...getInitialSendStateWithExistingTxState({
id: 1, id: 1,
fromAccount: { fromAccount: {
address: '0xAddress', address: mockAddress1,
}, },
}), }),
}, },
@ -2443,7 +2450,7 @@ describe('Send Slice', () => {
type: AssetType.native, type: AssetType.native,
}, },
fromAccount: { fromAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
gas: { gas: {
@ -2514,16 +2521,18 @@ describe('Send Slice', () => {
addressBook: { addressBook: {
[CHAIN_IDS.GOERLI]: {}, [CHAIN_IDS.GOERLI]: {},
}, },
identities: {}, identities: {
[mockAddress1]: {},
},
accounts: { accounts: {
'0xAddress': { [mockAddress1]: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}, },
cachedBalances: { cachedBalances: {
[CHAIN_IDS.GOERLI]: { [CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0', [mockAddress1]: '0x0',
}, },
}, },
tokenList: {}, tokenList: {},
@ -2533,10 +2542,10 @@ describe('Send Slice', () => {
txParams: { txParams: {
data: generateERC721TransferData({ data: generateERC721TransferData({
toAddress: BURN_ADDRESS, toAddress: BURN_ADDRESS,
fromAddress: '0xAddress', fromAddress: mockAddress1,
tokenId: BigNumber.from(15000).toString(), tokenId: BigNumber.from(15000).toString(),
}), }),
from: '0xAddress', from: mockAddress1,
to: '0xNftAddress', to: '0xNftAddress',
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
gasPrice: '0x3b9aca00', // 1000000000 gasPrice: '0x3b9aca00', // 1000000000
@ -2586,7 +2595,7 @@ describe('Send Slice', () => {
type: AssetType.native, type: AssetType.native,
}, },
fromAccount: { fromAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
gas: { gas: {
@ -2697,16 +2706,18 @@ describe('Send Slice', () => {
addressBook: { addressBook: {
[CHAIN_IDS.GOERLI]: {}, [CHAIN_IDS.GOERLI]: {},
}, },
identities: {}, identities: {
[mockAddress1]: {},
},
accounts: { accounts: {
'0xAddress': { [mockAddress1]: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
}, },
cachedBalances: { cachedBalances: {
[CHAIN_IDS.GOERLI]: { [CHAIN_IDS.GOERLI]: {
'0xAddress': '0x0', [mockAddress1]: '0x0',
}, },
}, },
unapprovedTxs: { unapprovedTxs: {
@ -2722,7 +2733,7 @@ describe('Send Slice', () => {
decimals: 18, decimals: 18,
}, },
}), }),
from: '0xAddress', from: mockAddress1,
to: '0xTokenAddress', to: '0xTokenAddress',
gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE,
gasPrice: '0x3b9aca00', // 1000000000 gasPrice: '0x3b9aca00', // 1000000000
@ -2740,7 +2751,7 @@ describe('Send Slice', () => {
}, },
}), }),
selectedAccount: { selectedAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
stage: SEND_STAGES.EDIT, stage: SEND_STAGES.EDIT,
@ -2777,7 +2788,7 @@ describe('Send Slice', () => {
type: AssetType.native, type: AssetType.native,
}, },
fromAccount: { fromAccount: {
address: '0xAddress', address: mockAddress1,
balance: '0x0', balance: '0x0',
}, },
gas: { gas: {

View File

@ -281,28 +281,38 @@ export function deprecatedGetCurrentNetworkId(state) {
return state.metamask.networkId ?? 'loading'; return state.metamask.networkId ?? 'loading';
} }
/**
* Get MetaMask accounts, including account name and balance.
*/
export const getMetaMaskAccounts = createSelector( export const getMetaMaskAccounts = createSelector(
getMetaMaskAccountsRaw, getMetaMaskIdentities,
getMetaMaskAccountBalances,
getMetaMaskCachedBalances, getMetaMaskCachedBalances,
(currentAccounts, cachedBalances) => (identities, balances, cachedBalances) =>
Object.entries(currentAccounts).reduce( Object.keys(identities).reduce((accounts, address) => {
(selectedAccounts, [accountID, account]) => { // TODO: mix in the identity state here as well, consolidating this
if (account.balance === null || account.balance === undefined) { // selector with `accountsWithSendEtherInfoSelector`
return { let account = {};
...selectedAccounts,
[accountID]: { if (balances[address]) {
account = {
...account, ...account,
balance: cachedBalances && cachedBalances[accountID], ...balances[address],
},
}; };
} }
return {
...selectedAccounts, if (account.balance === null || account.balance === undefined) {
[accountID]: account, account = {
...account,
balance: cachedBalances && cachedBalances[address],
}; };
}, }
{},
), return {
...accounts,
[address]: account,
};
}, {}),
); );
export function getSelectedAddress(state) { export function getSelectedAddress(state) {
@ -325,11 +335,23 @@ export function getMetaMaskKeyrings(state) {
return state.metamask.keyrings; return state.metamask.keyrings;
} }
/**
* Get identity state.
*
* @param {object} state - Redux state
* @returns {object} A map of account addresses to identities (which includes the account name)
*/
export function getMetaMaskIdentities(state) { export function getMetaMaskIdentities(state) {
return state.metamask.identities; return state.metamask.identities;
} }
export function getMetaMaskAccountsRaw(state) { /**
* Get account balances state.
*
* @param {object} state - Redux state
* @returns {object} A map of account addresses to account objects (which includes the account balance)
*/
export function getMetaMaskAccountBalances(state) {
return state.metamask.accounts; return state.metamask.accounts;
} }
@ -368,7 +390,7 @@ export const getMetaMaskAccountsConnected = createSelector(
export function isBalanceCached(state) { export function isBalanceCached(state) {
const selectedAccountBalance = const selectedAccountBalance =
state.metamask.accounts[getSelectedAddress(state)].balance; getMetaMaskAccountBalances(state)[getSelectedAddress(state)]?.balance;
const cachedBalance = getSelectedAccountCachedBalance(state); const cachedBalance = getSelectedAccountCachedBalance(state);
return Boolean(!selectedAccountBalance && cachedBalance); return Boolean(!selectedAccountBalance && cachedBalance);

View File

@ -244,6 +244,9 @@ describe('Actions', () => {
'0xAnotherAddress': '0x0', '0xAnotherAddress': '0x0',
}, },
}, },
identities: {
'0xAnotherAddress': {},
},
}), }),
); );
@ -1876,6 +1879,9 @@ describe('Actions', () => {
balance: '0x0', balance: '0x0',
}, },
}, },
identities: {
'0xFirstAddress': {},
},
cachedBalances: { cachedBalances: {
'0x1': { '0x1': {
'0xFirstAddress': '0x0', '0xFirstAddress': '0x0',