Skip to content

Commit 5f76122

Browse files
chore: cherry-pick #9993 (#10006)
This PR cherry-picks #9993 Co-authored-by: Cal Leung <[email protected]>
1 parent bea9d7c commit 5f76122

File tree

5 files changed

+191
-41
lines changed

5 files changed

+191
-41
lines changed

app/selectors/accountsController.test.ts

Lines changed: 67 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AccountsControllerState } from '@metamask/accounts-controller';
22
import { captureException } from '@sentry/react-native';
33
import { Hex, isValidChecksumAddress } from '@metamask/utils';
4+
import { InternalAccount } from '@metamask/keyring-api';
45
import DefaultPreference from 'react-native-default-preference';
56
import {
67
selectSelectedInternalAccount,
@@ -13,9 +14,48 @@ import {
1314
expectedUuid2,
1415
internalAccount1,
1516
MOCK_ADDRESS_2,
17+
createMockInternalAccount,
18+
createMockUuidFromAddress,
1619
} from '../util/test/accountsControllerTestUtils';
1720
import { RootState } from '../reducers';
1821
import { AGREED } from '../constants/storage';
22+
import {
23+
MOCK_KEYRINGS,
24+
MOCK_KEYRING_CONTROLLER,
25+
} from './keyringController/testUtils';
26+
27+
/**
28+
* Generates a mocked AccountsController state
29+
* The internal accounts are generated in reverse order relative to the mock keyrings that are used for generation
30+
*
31+
* @returns - A mocked state of AccountsController
32+
*/
33+
const MOCK_GENERATED_ACCOUNTS_CONTROLLER_REVERSED =
34+
(): AccountsControllerState => {
35+
const reversedKeyringAccounts = [...MOCK_KEYRINGS]
36+
.reverse()
37+
.flatMap((keyring) => [...keyring.accounts].reverse());
38+
const accountsForInternalAccounts = reversedKeyringAccounts.reduce(
39+
(record, keyringAccount, index) => {
40+
const lowercasedKeyringAccount = keyringAccount.toLowerCase();
41+
const accountName = `Account ${index}`;
42+
const uuid = createMockUuidFromAddress(lowercasedKeyringAccount);
43+
const internalAccount = createMockInternalAccount(
44+
lowercasedKeyringAccount,
45+
accountName,
46+
);
47+
record[uuid] = internalAccount;
48+
return record;
49+
},
50+
{} as Record<string, InternalAccount>,
51+
);
52+
return {
53+
internalAccounts: {
54+
accounts: accountsForInternalAccounts,
55+
selectedAccount: Object.values(accountsForInternalAccounts)[0].id,
56+
},
57+
};
58+
};
1959

2060
jest.mock('@sentry/react-native', () => ({
2161
captureException: jest.fn(),
@@ -84,47 +124,35 @@ describe('Accounts Controller Selectors', () => {
84124
});
85125
});
86126
describe('selectInternalAccounts', () => {
87-
it('returns all internal accounts in the accounts controller state', () => {
88-
expect(
89-
selectInternalAccounts({
90-
engine: {
91-
backgroundState: {
92-
AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
93-
},
127+
it(`returns internal accounts of the accounts controller sorted by the keyring controller's accounts`, () => {
128+
const mockAccountsControllerReversed =
129+
MOCK_GENERATED_ACCOUNTS_CONTROLLER_REVERSED();
130+
const internalAccountsResult = selectInternalAccounts({
131+
engine: {
132+
backgroundState: {
133+
KeyringController: MOCK_KEYRING_CONTROLLER,
134+
AccountsController: mockAccountsControllerReversed,
94135
},
95-
} as RootState),
96-
).toEqual([
97-
{
98-
address: '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272',
99-
id: expectedUuid,
100-
metadata: { name: 'Account 1', keyring: { type: 'HD Key Tree' } },
101-
options: {},
102-
methods: [
103-
'personal_sign',
104-
'eth_sign',
105-
'eth_signTransaction',
106-
'eth_signTypedData_v1',
107-
'eth_signTypedData_v3',
108-
'eth_signTypedData_v4',
109-
],
110-
type: 'eip155:eoa',
111-
},
112-
{
113-
address: '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756',
114-
id: expectedUuid2,
115-
metadata: { name: 'Account 2', keyring: { type: 'HD Key Tree' } },
116-
options: {},
117-
methods: [
118-
'personal_sign',
119-
'eth_sign',
120-
'eth_signTransaction',
121-
'eth_signTypedData_v1',
122-
'eth_signTypedData_v3',
123-
'eth_signTypedData_v4',
124-
],
125-
type: 'eip155:eoa',
126136
},
127-
]);
137+
} as RootState);
138+
const expectedInteralAccountsResult = Object.values(
139+
mockAccountsControllerReversed.internalAccounts.accounts,
140+
).reverse();
141+
142+
const internalAccountAddressesResult = internalAccountsResult.map(
143+
(account) => account.address,
144+
);
145+
const expectedAccountAddressesResult = [...MOCK_KEYRINGS].flatMap(
146+
(keyring) => keyring.accounts,
147+
);
148+
149+
// Ensure accounts are correct
150+
expect(internalAccountsResult).toEqual(expectedInteralAccountsResult);
151+
152+
// Ensure that order of internal accounts match order of keyring accounts
153+
expect(internalAccountAddressesResult).toEqual(
154+
expectedAccountAddressesResult,
155+
);
128156
});
129157
});
130158
describe('selectSelectedInternalAccountChecksummedAddress', () => {

app/selectors/accountsController.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,43 @@ import { captureException } from '@sentry/react-native';
44
import { createSelector } from 'reselect';
55
import { RootState } from '../reducers';
66
import { createDeepEqualSelector } from './util';
7+
import { selectFlattenedKeyringAccounts } from './keyringController';
78

9+
/**
10+
*
11+
* @param state - Root redux state
12+
* @returns - AccountsController state
13+
*/
814
const selectAccountsControllerState = (state: RootState) =>
915
state.engine.backgroundState.AccountsController;
1016

17+
/**
18+
* A memoized selector that returns internal accounts from the AccountsController, sorted by the order of KeyringController's keyring accounts
19+
*/
1120
export const selectInternalAccounts = createDeepEqualSelector(
1221
selectAccountsControllerState,
13-
(accountControllerState) =>
14-
Object.values(accountControllerState.internalAccounts.accounts),
22+
selectFlattenedKeyringAccounts,
23+
(accountControllerState, orderedKeyringAccounts) => {
24+
const keyringAccountsMap = new Map(
25+
orderedKeyringAccounts.map((account, index) => [
26+
account.toLowerCase(),
27+
index,
28+
]),
29+
);
30+
const sortedAccounts = Object.values(
31+
accountControllerState.internalAccounts.accounts,
32+
).sort(
33+
(a, b) =>
34+
(keyringAccountsMap.get(a.address.toLowerCase()) || 0) -
35+
(keyringAccountsMap.get(b.address.toLowerCase()) || 0),
36+
);
37+
return sortedAccounts;
38+
},
1539
);
1640

41+
/**
42+
* A memoized selector that returns the selected internal account from the AccountsController
43+
*/
1744
export const selectSelectedInternalAccount = createDeepEqualSelector(
1845
selectAccountsControllerState,
1946
(accountsControllerState: AccountsControllerState) => {
@@ -31,6 +58,9 @@ export const selectSelectedInternalAccount = createDeepEqualSelector(
3158
},
3259
);
3360

61+
/**
62+
* A memoized selector that returns the selected internal account address in checksum format
63+
*/
3464
export const selectSelectedInternalAccountChecksummedAddress = createSelector(
3565
selectSelectedInternalAccount,
3666
(account) => {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { selectKeyrings, selectFlattenedKeyringAccounts } from './index';
2+
import { RootState } from '../../reducers';
3+
import {
4+
MOCK_SIMPLE_ACCOUNTS,
5+
MOCK_QR_ACCOUNTS,
6+
MOCK_HD_ACCOUNTS,
7+
MOCK_KEYRINGS,
8+
MOCK_KEYRING_CONTROLLER,
9+
} from './testUtils';
10+
11+
describe('KeyringController Selectors', () => {
12+
describe('selectKeyrings', () => {
13+
it('returns keyrings', () => {
14+
expect(
15+
selectKeyrings({
16+
engine: {
17+
backgroundState: {
18+
KeyringController: MOCK_KEYRING_CONTROLLER,
19+
},
20+
},
21+
} as RootState),
22+
).toEqual(MOCK_KEYRINGS);
23+
});
24+
});
25+
describe('selectFlattenedKeyringAccounts', () => {
26+
it('returns flattened keyring accounts', () => {
27+
const expectedOrderedKeyringAccounts = [
28+
...MOCK_SIMPLE_ACCOUNTS,
29+
...MOCK_QR_ACCOUNTS,
30+
...MOCK_HD_ACCOUNTS,
31+
];
32+
expect(
33+
selectFlattenedKeyringAccounts({
34+
engine: {
35+
backgroundState: {
36+
KeyringController: MOCK_KEYRING_CONTROLLER,
37+
},
38+
},
39+
} as RootState),
40+
).toEqual(expectedOrderedKeyringAccounts);
41+
});
42+
});
43+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { RootState } from '../../reducers';
2+
import { createDeepEqualSelector } from '../util';
3+
4+
/**
5+
*
6+
* @param state - Root Redux state
7+
* @returns - KeyringController state
8+
*/
9+
const selectKeyringControllerState = (state: RootState) =>
10+
state.engine.backgroundState.KeyringController;
11+
12+
/**
13+
* A memoized selector that retrieves keyrings from the KeyringController
14+
*/
15+
export const selectKeyrings = createDeepEqualSelector(
16+
selectKeyringControllerState,
17+
(keyringControllerState) => keyringControllerState.keyrings,
18+
);
19+
20+
/**
21+
* A memoized selector that returns the list of accounts from all keyrings in the form of a flattened array of strings.
22+
*/
23+
export const selectFlattenedKeyringAccounts = createDeepEqualSelector(
24+
selectKeyrings,
25+
(keyrings) => {
26+
const flattenedKeyringAccounts = keyrings.flatMap(
27+
(keyring) => keyring.accounts,
28+
);
29+
return flattenedKeyringAccounts;
30+
},
31+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {
2+
KeyringControllerState,
3+
KeyringObject,
4+
KeyringTypes,
5+
} from '@metamask/keyring-controller';
6+
7+
export const MOCK_SIMPLE_ACCOUNTS = ['0x1', '0x2'];
8+
export const MOCK_QR_ACCOUNTS = ['0x3', '0x4'];
9+
export const MOCK_HD_ACCOUNTS = ['0x5', '0x6'];
10+
export const MOCK_KEYRINGS: KeyringObject[] = [
11+
{ accounts: MOCK_SIMPLE_ACCOUNTS, type: KeyringTypes.simple },
12+
{ accounts: MOCK_QR_ACCOUNTS, type: KeyringTypes.qr },
13+
{ accounts: MOCK_HD_ACCOUNTS, type: KeyringTypes.hd },
14+
];
15+
export const MOCK_KEYRING_CONTROLLER: KeyringControllerState = {
16+
isUnlocked: true,
17+
keyrings: MOCK_KEYRINGS,
18+
};

0 commit comments

Comments
 (0)