Skip to content

Commit 5761ee7

Browse files
montelaidevccharly
andauthored
fix: update handleAccountRemoved logic (#4322)
## Explanation This PR modifies the handleAccountRemoved function to update the selected account to the most recent one, if the account being removed is currently the selected account, all within the same update. ## References Related to MetaMask/metamask-mobile#9749 ## Changelog ### `@metamask/accounts-controller` - **FIXED**: Updates handleAccountRemoved to update the selected account to the most recent one, if the account being removed is currently the selected account, all within the same step. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Charly Chevalier <[email protected]>
1 parent 9d738ef commit 5761ee7

File tree

3 files changed

+128
-141
lines changed

3 files changed

+128
-141
lines changed

packages/accounts-controller/src/AccountsController.test.ts

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,16 +1078,10 @@ describe('AccountsController', () => {
10781078
initialState: {
10791079
internalAccounts: {
10801080
accounts: {
1081-
'missing-account': {
1082-
id: 'missing-account',
1081+
'missing-account': createMockInternalAccount({
10831082
address: '0x999',
1084-
metadata: {
1085-
keyring: {
1086-
type: KeyringTypes.hd,
1087-
},
1088-
},
1089-
[mockAccount2.id]: mockAccount2WithoutLastSelected,
1090-
} as unknown as InternalAccount,
1083+
id: 'missing-account',
1084+
}),
10911085
[mockAccount.id]: mockAccountWithoutLastSelected,
10921086
[mockAccount2.id]: mockAccount2WithoutLastSelected,
10931087
},
@@ -1850,6 +1844,32 @@ describe('AccountsController', () => {
18501844
'No EVM accounts',
18511845
);
18521846
});
1847+
1848+
it('handle the edge case of undefined accountId during onboarding', async () => {
1849+
const { accountsController } = setupAccountsController({
1850+
initialState: {
1851+
internalAccounts: {
1852+
accounts: {},
1853+
selectedAccount: '',
1854+
},
1855+
},
1856+
});
1857+
1858+
expect(accountsController.getSelectedAccount()).toStrictEqual({
1859+
id: '',
1860+
address: '',
1861+
options: {},
1862+
methods: [],
1863+
type: EthAccountType.Eoa,
1864+
metadata: {
1865+
name: '',
1866+
keyring: {
1867+
type: '',
1868+
},
1869+
importTime: 0,
1870+
},
1871+
});
1872+
});
18531873
});
18541874

18551875
describe('getSelectedMultichainAccount', () => {
@@ -2060,33 +2080,6 @@ describe('AccountsController', () => {
20602080
`Account Id "${accountId}" not found`,
20612081
);
20622082
});
2063-
2064-
it('handle the edge case of undefined accountId during onboarding', async () => {
2065-
const { accountsController } = setupAccountsController({
2066-
initialState: {
2067-
internalAccounts: {
2068-
accounts: { [mockAccount.id]: mockAccount },
2069-
selectedAccount: mockAccount.id,
2070-
},
2071-
},
2072-
});
2073-
2074-
// @ts-expect-error forcing undefined accountId
2075-
expect(accountsController.getAccountExpect(undefined)).toStrictEqual({
2076-
id: '',
2077-
address: '',
2078-
options: {},
2079-
methods: [],
2080-
type: EthAccountType.Eoa,
2081-
metadata: {
2082-
name: '',
2083-
keyring: {
2084-
type: '',
2085-
},
2086-
importTime: 0,
2087-
},
2088-
});
2089-
});
20902083
});
20912084

20922085
describe('setSelectedAccount', () => {

packages/accounts-controller/src/AccountsController.ts

Lines changed: 99 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
import type { Draft } from 'immer';
3636

3737
import {
38-
deepCloneDraft,
3938
getUUIDFromAddressOfNormalAccount,
4039
isNormalKeyringType,
4140
keyringTypeToName,
@@ -271,9 +270,22 @@ export class AccountsController extends BaseController<
271270
* @throws An error if the account ID is not found.
272271
*/
273272
getAccountExpect(accountId: string): InternalAccount {
273+
const account = this.getAccount(accountId);
274+
if (account === undefined) {
275+
throw new Error(`Account Id "${accountId}" not found`);
276+
}
277+
return account;
278+
}
279+
280+
/**
281+
* Returns the last selected EVM account.
282+
*
283+
* @returns The selected internal account.
284+
*/
285+
getSelectedAccount(): InternalAccount {
274286
// Edge case where the extension is setup but the srp is not yet created
275287
// certain ui elements will query the selected address before any accounts are created.
276-
if (!accountId) {
288+
if (this.state.internalAccounts.selectedAccount === '') {
277289
return {
278290
id: '',
279291
address: '',
@@ -290,19 +302,6 @@ export class AccountsController extends BaseController<
290302
};
291303
}
292304

293-
const account = this.getAccount(accountId);
294-
if (account === undefined) {
295-
throw new Error(`Account Id "${accountId}" not found`);
296-
}
297-
return account;
298-
}
299-
300-
/**
301-
* Returns the last selected evm account.
302-
*
303-
* @returns The selected internal account.
304-
*/
305-
getSelectedAccount(): InternalAccount {
306305
const selectedAccount = this.getAccountExpect(
307306
this.state.internalAccounts.selectedAccount,
308307
);
@@ -412,12 +411,7 @@ export class AccountsController extends BaseController<
412411
...account,
413412
metadata: { ...account.metadata, name: accountName },
414413
};
415-
// FIXME: deep clone of old state to get around Type instantiation is excessively deep and possibly infinite.
416-
const newState = deepCloneDraft(currentState);
417-
418-
newState.internalAccounts.accounts[accountId] = internalAccount;
419-
420-
return newState;
414+
currentState.internalAccounts.accounts[accountId] = internalAccount;
421415
});
422416
}
423417

@@ -474,12 +468,7 @@ export class AccountsController extends BaseController<
474468
}, {} as Record<string, InternalAccount>);
475469

476470
this.update((currentState: Draft<AccountsControllerState>) => {
477-
// FIXME: deep clone of old state to get around Type instantiation is excessively deep and possibly infinite.
478-
const newState = deepCloneDraft(currentState);
479-
480-
newState.internalAccounts.accounts = accounts;
481-
482-
return newState;
471+
currentState.internalAccounts.accounts = accounts;
483472
});
484473
}
485474

@@ -491,12 +480,7 @@ export class AccountsController extends BaseController<
491480
loadBackup(backup: AccountsControllerState): void {
492481
if (backup.internalAccounts) {
493482
this.update((currentState: Draft<AccountsControllerState>) => {
494-
// FIXME: deep clone of old state to get around Type instantiation is excessively deep and possibly infinite.
495-
const newState = deepCloneDraft(currentState);
496-
497-
newState.internalAccounts = backup.internalAccounts;
498-
499-
return newState;
483+
currentState.internalAccounts = backup.internalAccounts;
500484
});
501485
}
502486
}
@@ -717,41 +701,58 @@ export class AccountsController extends BaseController<
717701
}
718702
}
719703

720-
if (deletedAccounts.length > 0) {
721-
for (const account of deletedAccounts) {
722-
this.#handleAccountRemoved(account.id);
704+
this.update((currentState: Draft<AccountsControllerState>) => {
705+
if (deletedAccounts.length > 0) {
706+
for (const account of deletedAccounts) {
707+
currentState.internalAccounts.accounts = this.#handleAccountRemoved(
708+
currentState.internalAccounts.accounts,
709+
account.id,
710+
);
711+
}
723712
}
724-
}
725713

726-
if (addedAccounts.length > 0) {
727-
for (const account of addedAccounts) {
728-
this.#handleNewAccountAdded(account);
714+
if (addedAccounts.length > 0) {
715+
for (const account of addedAccounts) {
716+
currentState.internalAccounts.accounts =
717+
this.#handleNewAccountAdded(
718+
currentState.internalAccounts.accounts,
719+
account,
720+
);
721+
}
729722
}
730-
}
731723

732-
// handle if the selected account was deleted
733-
if (!this.getAccount(this.state.internalAccounts.selectedAccount)) {
734-
const [accountToSelect] = this.listAccounts().sort(
735-
(accountA, accountB) => {
736-
// sort by lastSelected descending
737-
return (
738-
(accountB.metadata.lastSelected ?? 0) -
739-
(accountA.metadata.lastSelected ?? 0)
740-
);
741-
},
724+
// We don't use list accounts because it is not the updated state yet.
725+
const existingAccounts = Object.values(
726+
currentState.internalAccounts.accounts,
742727
);
743728

744-
// if the accountToSelect is undefined, then there are no accounts
745-
// it mean the keyring was reinitialized.
746-
if (!accountToSelect) {
747-
this.update((currentState: Draft<AccountsControllerState>) => {
729+
// handle if the selected account was deleted
730+
if (
731+
!currentState.internalAccounts.accounts[
732+
this.state.internalAccounts.selectedAccount
733+
]
734+
) {
735+
// if currently selected account is undefined and there are no accounts
736+
// it mean the keyring was reinitialized.
737+
if (existingAccounts.length === 0) {
748738
currentState.internalAccounts.selectedAccount = '';
749-
});
750-
return;
751-
}
739+
return;
740+
}
752741

753-
this.setSelectedAccount(accountToSelect.id);
754-
}
742+
// at this point, we know that `existingAccounts.length` is > 0, so
743+
// `accountToSelect` won't be `undefined`!
744+
const [accountToSelect] = existingAccounts.sort(
745+
(accountA, accountB) => {
746+
// sort by lastSelected descending
747+
return (
748+
(accountB.metadata.lastSelected ?? 0) -
749+
(accountA.metadata.lastSelected ?? 0)
750+
);
751+
},
752+
);
753+
currentState.internalAccounts.selectedAccount = accountToSelect.id;
754+
}
755+
});
755756
}
756757
}
757758

@@ -786,10 +787,11 @@ export class AccountsController extends BaseController<
786787
/**
787788
* Returns the list of accounts for a given keyring type.
788789
* @param keyringType - The type of keyring.
790+
* @param accounts - Accounts to filter by keyring type.
789791
* @returns The list of accounts associcated with this keyring type.
790792
*/
791-
#getAccountsByKeyringType(keyringType: string) {
792-
return this.listAccounts().filter((internalAccount) => {
793+
#getAccountsByKeyringType(keyringType: string, accounts?: InternalAccount[]) {
794+
return (accounts ?? this.listAccounts()).filter((internalAccount) => {
793795
// We do consider `hd` and `simple` keyrings to be of same type. So we check those 2 types
794796
// to group those accounts together!
795797
if (
@@ -833,11 +835,18 @@ export class AccountsController extends BaseController<
833835
/**
834836
* Returns the next account number for a given keyring type.
835837
* @param keyringType - The type of keyring.
838+
* @param accounts - Existing accounts to check for the next available account number.
836839
* @returns An object containing the account prefix and index to use.
837840
*/
838-
getNextAvailableAccountName(keyringType: string = KeyringTypes.hd): string {
841+
getNextAvailableAccountName(
842+
keyringType: string = KeyringTypes.hd,
843+
accounts?: InternalAccount[],
844+
): string {
839845
const keyringName = keyringTypeToName(keyringType);
840-
const keyringAccounts = this.#getAccountsByKeyringType(keyringType);
846+
const keyringAccounts = this.#getAccountsByKeyringType(
847+
keyringType,
848+
accounts,
849+
);
841850
const lastDefaultIndexUsedForKeyringType = keyringAccounts.reduce(
842851
(maxInternalAccountIndex, internalAccount) => {
843852
// We **DO NOT USE** `\d+` here to only consider valid "human"
@@ -888,9 +897,14 @@ export class AccountsController extends BaseController<
888897
* Handles the addition of a new account to the controller.
889898
* If the account is not a Snap Keyring account, generates an internal account for it and adds it to the controller.
890899
* If the account is a Snap Keyring account, retrieves the account from the keyring and adds it to the controller.
900+
* @param accountsState - AccountsController accounts state that is to be mutated.
891901
* @param account - The address and keyring type object of the new account.
902+
* @returns The updated AccountsController accounts state.
892903
*/
893-
#handleNewAccountAdded(account: AddressAndKeyringTypeObject) {
904+
#handleNewAccountAdded(
905+
accountsState: AccountsControllerState['internalAccounts']['accounts'],
906+
account: AddressAndKeyringTypeObject,
907+
): AccountsControllerState['internalAccounts']['accounts'] {
894908
let newAccount: InternalAccount;
895909
if (account.type !== KeyringTypes.snap) {
896910
newAccount = this.#generateInternalAccountForNonSnapAccount(
@@ -909,41 +923,41 @@ export class AccountsController extends BaseController<
909923

910924
// The snap deleted the account before the keyring controller could add it
911925
if (!newAccount) {
912-
return;
926+
return accountsState;
913927
}
914928
}
915929

916930
// Get next account name available for this given keyring
917931
const accountName = this.getNextAvailableAccountName(
918932
newAccount.metadata.keyring.type,
933+
Object.values(accountsState),
919934
);
920935

921-
this.update((currentState: Draft<AccountsControllerState>) => {
922-
// FIXME: deep clone of old state to get around Type instantiation is excessively deep and possibly infinite.
923-
const newState = deepCloneDraft(currentState);
924-
925-
newState.internalAccounts.accounts[newAccount.id] = {
926-
...newAccount,
927-
metadata: {
928-
...newAccount.metadata,
929-
name: accountName,
930-
importTime: Date.now(),
931-
lastSelected: 0,
932-
},
933-
};
936+
accountsState[newAccount.id] = {
937+
...newAccount,
938+
metadata: {
939+
...newAccount.metadata,
940+
name: accountName,
941+
importTime: Date.now(),
942+
lastSelected: 0,
943+
},
944+
};
934945

935-
return newState;
936-
});
946+
return accountsState;
937947
}
938948

939949
/**
940950
* Handles the removal of an account from the internal accounts list.
951+
* @param accountsState - AccountsController accounts state that is to be mutated.
941952
* @param accountId - The ID of the account to be removed.
953+
* @returns The updated AccountsController state.
942954
*/
943-
#handleAccountRemoved(accountId: string) {
944-
this.update((currentState: Draft<AccountsControllerState>) => {
945-
delete currentState.internalAccounts.accounts[accountId];
946-
});
955+
#handleAccountRemoved(
956+
accountsState: AccountsControllerState['internalAccounts']['accounts'],
957+
accountId: string,
958+
): AccountsControllerState['internalAccounts']['accounts'] {
959+
delete accountsState[accountId];
960+
return accountsState;
947961
}
948962

949963
/**

0 commit comments

Comments
 (0)