Skip to content

Commit 3428d20

Browse files
committed
feat(accounts-controller): add new setAccountNameAndSelectAccount action
1 parent ce61b87 commit 3428d20

File tree

4 files changed

+214
-9
lines changed

4 files changed

+214
-9
lines changed

packages/accounts-controller/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add new `setAccountNameAndSelectAccount` action ([#5714](https://github.com/MetaMask/core/pull/5714))
13+
1014
### Changed
1115

1216
- **BREAKING:** Bump `@metamask/snaps-controllers` peer dependency from ^9.19.0 to ^11.0.0 ([#5639](https://github.com/MetaMask/core/pull/5639))

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

+145
Original file line numberDiff line numberDiff line change
@@ -2719,6 +2719,116 @@ describe('AccountsController', () => {
27192719
});
27202720
});
27212721

2722+
describe('setAccountNameAndSelect', () => {
2723+
const newAccountName = 'New Account Name';
2724+
const mockState = {
2725+
initialState: {
2726+
internalAccounts: {
2727+
accounts: { [mockAccount.id]: mockAccount },
2728+
selectedAccount: mockAccount.id,
2729+
},
2730+
},
2731+
};
2732+
2733+
it('sets the name of an existing account', () => {
2734+
const { accountsController } = setupAccountsController(mockState);
2735+
2736+
accountsController.setAccountNameAndSelectAccount(
2737+
mockAccount.id,
2738+
newAccountName,
2739+
);
2740+
2741+
expect(
2742+
accountsController.getAccountExpect(mockAccount.id).metadata.name,
2743+
).toBe(newAccountName);
2744+
expect(accountsController.state.internalAccounts.selectedAccount).toBe(
2745+
mockAccount.id,
2746+
);
2747+
});
2748+
2749+
it('sets the name of an existing account and select the account', () => {
2750+
const { accountsController } = setupAccountsController({
2751+
initialState: {
2752+
internalAccounts: {
2753+
accounts: {
2754+
[mockAccount.id]: mockAccount,
2755+
[mockAccount2.id]: mockAccount2,
2756+
},
2757+
selectedAccount: mockAccount.id,
2758+
},
2759+
},
2760+
});
2761+
2762+
accountsController.setAccountNameAndSelectAccount(
2763+
mockAccount2.id,
2764+
newAccountName,
2765+
);
2766+
2767+
expect(
2768+
accountsController.getAccountExpect(mockAccount2.id).metadata.name,
2769+
).toBe(newAccountName);
2770+
expect(accountsController.state.internalAccounts.selectedAccount).toBe(
2771+
mockAccount2.id,
2772+
);
2773+
});
2774+
2775+
it('sets the nameLastUpdatedAt timestamp when setting the name of an existing account', () => {
2776+
const expectedTimestamp = Number(new Date('2024-01-02'));
2777+
2778+
jest.spyOn(Date, 'now').mockImplementation(() => expectedTimestamp);
2779+
2780+
const { accountsController } = setupAccountsController(mockState);
2781+
2782+
accountsController.setAccountNameAndSelectAccount(
2783+
mockAccount.id,
2784+
newAccountName,
2785+
);
2786+
2787+
expect(
2788+
accountsController.getAccountExpect(mockAccount.id).metadata
2789+
.nameLastUpdatedAt,
2790+
).toBe(expectedTimestamp);
2791+
});
2792+
2793+
it('publishes the accountRenamed event', () => {
2794+
const { accountsController, messenger } =
2795+
setupAccountsController(mockState);
2796+
2797+
const messengerSpy = jest.spyOn(messenger, 'publish');
2798+
2799+
accountsController.setAccountNameAndSelectAccount(
2800+
mockAccount.id,
2801+
newAccountName,
2802+
);
2803+
2804+
expect(messengerSpy).toHaveBeenCalledWith(
2805+
'AccountsController:accountRenamed',
2806+
accountsController.getAccountExpect(mockAccount.id),
2807+
);
2808+
});
2809+
2810+
it('throw an error if the account name already exists', () => {
2811+
const { accountsController } = setupAccountsController({
2812+
initialState: {
2813+
internalAccounts: {
2814+
accounts: {
2815+
[mockAccount.id]: mockAccount,
2816+
[mockAccount2.id]: mockAccount2,
2817+
},
2818+
selectedAccount: mockAccount.id,
2819+
},
2820+
},
2821+
});
2822+
2823+
expect(() =>
2824+
accountsController.setAccountNameAndSelectAccount(
2825+
mockAccount.id,
2826+
mockAccount2.metadata.name,
2827+
),
2828+
).toThrow('Account name already exists');
2829+
});
2830+
});
2831+
27222832
describe('setAccountName', () => {
27232833
it('sets the name of an existing account', () => {
27242834
const { accountsController } = setupAccountsController({
@@ -3033,6 +3143,10 @@ describe('AccountsController', () => {
30333143
jest.spyOn(AccountsController.prototype, 'getAccountByAddress');
30343144
jest.spyOn(AccountsController.prototype, 'getSelectedAccount');
30353145
jest.spyOn(AccountsController.prototype, 'getAccount');
3146+
jest.spyOn(
3147+
AccountsController.prototype,
3148+
'setAccountNameAndSelectAccount',
3149+
);
30363150
});
30373151

30383152
describe('setSelectedAccount', () => {
@@ -3142,6 +3256,37 @@ describe('AccountsController', () => {
31423256
});
31433257
});
31443258

3259+
describe('setAccountNameAndSelectAccount', () => {
3260+
it('set the account name and select the account', async () => {
3261+
const messenger = buildMessenger();
3262+
const { accountsController } = setupAccountsController({
3263+
initialState: {
3264+
internalAccounts: {
3265+
accounts: {
3266+
[mockAccount.id]: mockAccount,
3267+
[mockAccount2.id]: mockAccount2,
3268+
},
3269+
selectedAccount: mockAccount.id,
3270+
},
3271+
},
3272+
messenger,
3273+
});
3274+
3275+
const newAccountName = 'New Account Name';
3276+
messenger.call(
3277+
'AccountsController:setAccountNameAndSelectAccount',
3278+
mockAccount2.id,
3279+
newAccountName,
3280+
);
3281+
expect(
3282+
accountsController.setAccountNameAndSelectAccount,
3283+
).toHaveBeenCalledWith(mockAccount2.id, newAccountName);
3284+
expect(accountsController.state.internalAccounts.selectedAccount).toBe(
3285+
mockAccount2.id,
3286+
);
3287+
});
3288+
});
3289+
31453290
describe('updateAccounts', () => {
31463291
it('update accounts', async () => {
31473292
const messenger = buildMessenger();

packages/accounts-controller/src/AccountsController.ts

+64-9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export type AccountsControllerSetAccountNameAction = {
6969
handler: AccountsController['setAccountName'];
7070
};
7171

72+
export type AccountsControllerSetAccountNameAndSelectAccountAction = {
73+
type: `${typeof controllerName}:setAccountNameAndSelectAccount`;
74+
handler: AccountsController['setAccountNameAndSelectAccount'];
75+
};
76+
7277
export type AccountsControllerListAccountsAction = {
7378
type: `${typeof controllerName}:listAccounts`;
7479
handler: AccountsController['listAccounts'];
@@ -124,6 +129,7 @@ export type AccountsControllerActions =
124129
| AccountsControllerListAccountsAction
125130
| AccountsControllerListMultichainAccountsAction
126131
| AccountsControllerSetAccountNameAction
132+
| AccountsControllerSetAccountNameAndSelectAccountAction
127133
| AccountsControllerUpdateAccountsAction
128134
| AccountsControllerGetAccountByAddressAction
129135
| AccountsControllerGetSelectedAccountAction
@@ -437,6 +443,57 @@ export class AccountsController extends BaseController<
437443
});
438444
}
439445

446+
/**
447+
* Sets the name of the account with the given ID and select it.
448+
*
449+
* @param accountId - The ID of the account to set the name for and select.
450+
* @param accountName - The new name for the account.
451+
* @throws An error if an account with the same name already exists.
452+
*/
453+
setAccountNameAndSelectAccount(accountId: string, accountName: string): void {
454+
const account = this.getAccountExpect(accountId);
455+
456+
this.#assertAccountCanBeRenamed(account, accountName);
457+
458+
const internalAccount = {
459+
...account,
460+
metadata: {
461+
...account.metadata,
462+
name: accountName,
463+
nameLastUpdatedAt: Date.now(),
464+
lastSelected: this.#getLastSelectedIndex(),
465+
},
466+
};
467+
468+
this.#update((state) => {
469+
// FIXME: Using the state as-is cause the following error: "Type instantiation is excessively
470+
// deep and possibly infinite.ts(2589)" (https://github.com/MetaMask/utils/issues/168)
471+
// Using a type-cast workaround this error and is slightly better than using a @ts-expect-error
472+
// which sometimes fail when compiling locally.
473+
(state as AccountsControllerState).internalAccounts.accounts[account.id] =
474+
internalAccount;
475+
(state as AccountsControllerState).internalAccounts.selectedAccount =
476+
account.id;
477+
});
478+
479+
this.messagingSystem.publish(
480+
'AccountsController:accountRenamed',
481+
internalAccount,
482+
);
483+
}
484+
485+
#assertAccountCanBeRenamed(account: InternalAccount, accountName: string) {
486+
if (
487+
this.listMultichainAccounts().find(
488+
(internalAccount) =>
489+
internalAccount.metadata.name === accountName &&
490+
internalAccount.id !== account.id,
491+
)
492+
) {
493+
throw new Error('Account name already exists');
494+
}
495+
}
496+
440497
/**
441498
* Updates the metadata of the account with the given ID.
442499
*
@@ -449,15 +506,8 @@ export class AccountsController extends BaseController<
449506
): void {
450507
const account = this.getAccountExpect(accountId);
451508

452-
if (
453-
metadata.name &&
454-
this.listMultichainAccounts().find(
455-
(internalAccount) =>
456-
internalAccount.metadata.name === metadata.name &&
457-
internalAccount.id !== accountId,
458-
)
459-
) {
460-
throw new Error('Account name already exists');
509+
if (metadata.name) {
510+
this.#assertAccountCanBeRenamed(account, metadata.name);
461511
}
462512

463513
const internalAccount = {
@@ -1197,6 +1247,11 @@ export class AccountsController extends BaseController<
11971247
this.setAccountName.bind(this),
11981248
);
11991249

1250+
this.messagingSystem.registerActionHandler(
1251+
`${controllerName}:setAccountNameAndSelectAccount`,
1252+
this.setAccountNameAndSelectAccount.bind(this),
1253+
);
1254+
12001255
this.messagingSystem.registerActionHandler(
12011256
`${controllerName}:updateAccounts`,
12021257
this.updateAccounts.bind(this),

packages/accounts-controller/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type {
44
AccountsControllerGetStateAction,
55
AccountsControllerSetSelectedAccountAction,
66
AccountsControllerSetAccountNameAction,
7+
AccountsControllerSetAccountNameAndSelectAccountAction,
78
AccountsControllerListAccountsAction,
89
AccountsControllerListMultichainAccountsAction,
910
AccountsControllerUpdateAccountsAction,

0 commit comments

Comments
 (0)