Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/accounts-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add new `setAccountNameAndSelectAccount` action ([#5714](https://github.com/MetaMask/core/pull/5714))

### Changed

- Bump `@metamask/base-controller` from ^8.0.0 to ^8.0.1 ([#5722](https://github.com/MetaMask/core/pull/5722))
Expand Down
145 changes: 145 additions & 0 deletions packages/accounts-controller/src/AccountsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import type { SnapControllerState } from '@metamask/snaps-controllers';
import { SnapStatus } from '@metamask/snaps-utils';
import type { CaipChainId } from '@metamask/utils';
import * as uuid from 'uuid';

Check warning on line 20 in packages/accounts-controller/src/AccountsController.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

No exported names found in module 'uuid'
import type { V4Options } from 'uuid';

import type {
Expand Down Expand Up @@ -2719,6 +2719,116 @@
});
});

describe('setAccountNameAndSelect', () => {
const newAccountName = 'New Account Name';
const mockState = {
initialState: {
internalAccounts: {
accounts: { [mockAccount.id]: mockAccount },
selectedAccount: mockAccount.id,
},
},
};

it('sets the name of an existing account', () => {
const { accountsController } = setupAccountsController(mockState);

accountsController.setAccountNameAndSelectAccount(
mockAccount.id,
newAccountName,
);

expect(
accountsController.getAccountExpect(mockAccount.id).metadata.name,
).toBe(newAccountName);
expect(accountsController.state.internalAccounts.selectedAccount).toBe(
mockAccount.id,
);
});

it('sets the name of an existing account and select the account', () => {
const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
[mockAccount.id]: mockAccount,
[mockAccount2.id]: mockAccount2,
},
selectedAccount: mockAccount.id,
},
},
});

accountsController.setAccountNameAndSelectAccount(
mockAccount2.id,
newAccountName,
);

expect(
accountsController.getAccountExpect(mockAccount2.id).metadata.name,
).toBe(newAccountName);
expect(accountsController.state.internalAccounts.selectedAccount).toBe(
mockAccount2.id,
);
});

it('sets the nameLastUpdatedAt timestamp when setting the name of an existing account', () => {
const expectedTimestamp = Number(new Date('2024-01-02'));

jest.spyOn(Date, 'now').mockImplementation(() => expectedTimestamp);

const { accountsController } = setupAccountsController(mockState);

accountsController.setAccountNameAndSelectAccount(
mockAccount.id,
newAccountName,
);

expect(
accountsController.getAccountExpect(mockAccount.id).metadata
.nameLastUpdatedAt,
).toBe(expectedTimestamp);
});

it('publishes the accountRenamed event', () => {
const { accountsController, messenger } =
setupAccountsController(mockState);

const messengerSpy = jest.spyOn(messenger, 'publish');

accountsController.setAccountNameAndSelectAccount(
mockAccount.id,
newAccountName,
);

expect(messengerSpy).toHaveBeenCalledWith(
'AccountsController:accountRenamed',
accountsController.getAccountExpect(mockAccount.id),
);
});

it('throw an error if the account name already exists', () => {
const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
[mockAccount.id]: mockAccount,
[mockAccount2.id]: mockAccount2,
},
selectedAccount: mockAccount.id,
},
},
});

expect(() =>
accountsController.setAccountNameAndSelectAccount(
mockAccount.id,
mockAccount2.metadata.name,
),
).toThrow('Account name already exists');
});
});

describe('setAccountName', () => {
it('sets the name of an existing account', () => {
const { accountsController } = setupAccountsController({
Expand Down Expand Up @@ -3033,6 +3143,10 @@
jest.spyOn(AccountsController.prototype, 'getAccountByAddress');
jest.spyOn(AccountsController.prototype, 'getSelectedAccount');
jest.spyOn(AccountsController.prototype, 'getAccount');
jest.spyOn(
AccountsController.prototype,
'setAccountNameAndSelectAccount',
);
});

describe('setSelectedAccount', () => {
Expand Down Expand Up @@ -3142,6 +3256,37 @@
});
});

describe('setAccountNameAndSelectAccount', () => {
it('set the account name and select the account', async () => {
const messenger = buildMessenger();
const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
[mockAccount.id]: mockAccount,
[mockAccount2.id]: mockAccount2,
},
selectedAccount: mockAccount.id,
},
},
messenger,
});

const newAccountName = 'New Account Name';
messenger.call(
'AccountsController:setAccountNameAndSelectAccount',
mockAccount2.id,
newAccountName,
);
expect(
accountsController.setAccountNameAndSelectAccount,
).toHaveBeenCalledWith(mockAccount2.id, newAccountName);
expect(accountsController.state.internalAccounts.selectedAccount).toBe(
mockAccount2.id,
);
});
});

describe('updateAccounts', () => {
it('update accounts', async () => {
const messenger = buildMessenger();
Expand Down
73 changes: 64 additions & 9 deletions packages/accounts-controller/src/AccountsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export type AccountsControllerSetAccountNameAction = {
handler: AccountsController['setAccountName'];
};

export type AccountsControllerSetAccountNameAndSelectAccountAction = {
type: `${typeof controllerName}:setAccountNameAndSelectAccount`;
handler: AccountsController['setAccountNameAndSelectAccount'];
};

export type AccountsControllerListAccountsAction = {
type: `${typeof controllerName}:listAccounts`;
handler: AccountsController['listAccounts'];
Expand Down Expand Up @@ -124,6 +129,7 @@ export type AccountsControllerActions =
| AccountsControllerListAccountsAction
| AccountsControllerListMultichainAccountsAction
| AccountsControllerSetAccountNameAction
| AccountsControllerSetAccountNameAndSelectAccountAction
| AccountsControllerUpdateAccountsAction
| AccountsControllerGetAccountByAddressAction
| AccountsControllerGetSelectedAccountAction
Expand Down Expand Up @@ -437,6 +443,57 @@ export class AccountsController extends BaseController<
});
}

/**
* Sets the name of the account with the given ID and select it.
*
* @param accountId - The ID of the account to set the name for and select.
* @param accountName - The new name for the account.
* @throws An error if an account with the same name already exists.
*/
setAccountNameAndSelectAccount(accountId: string, accountName: string): void {
const account = this.getAccountExpect(accountId);

this.#assertAccountCanBeRenamed(account, accountName);

const internalAccount = {
...account,
metadata: {
...account.metadata,
name: accountName,
nameLastUpdatedAt: Date.now(),
lastSelected: this.#getLastSelectedIndex(),
},
};

this.#update((state) => {
// FIXME: Using the state as-is cause the following error: "Type instantiation is excessively
// deep and possibly infinite.ts(2589)" (https://github.com/MetaMask/utils/issues/168)
// Using a type-cast workaround this error and is slightly better than using a @ts-expect-error
// which sometimes fail when compiling locally.
(state as AccountsControllerState).internalAccounts.accounts[account.id] =
internalAccount;
(state as AccountsControllerState).internalAccounts.selectedAccount =
account.id;
});

this.messagingSystem.publish(
'AccountsController:accountRenamed',
internalAccount,
);
}

#assertAccountCanBeRenamed(account: InternalAccount, accountName: string) {
if (
this.listMultichainAccounts().find(
(internalAccount) =>
internalAccount.metadata.name === accountName &&
internalAccount.id !== account.id,
)
) {
throw new Error('Account name already exists');
}
}

/**
* Updates the metadata of the account with the given ID.
*
Expand All @@ -449,15 +506,8 @@ export class AccountsController extends BaseController<
): void {
const account = this.getAccountExpect(accountId);

if (
metadata.name &&
this.listMultichainAccounts().find(
(internalAccount) =>
internalAccount.metadata.name === metadata.name &&
internalAccount.id !== accountId,
)
) {
throw new Error('Account name already exists');
if (metadata.name) {
this.#assertAccountCanBeRenamed(account, metadata.name);
}

const internalAccount = {
Expand Down Expand Up @@ -1197,6 +1247,11 @@ export class AccountsController extends BaseController<
this.setAccountName.bind(this),
);

this.messagingSystem.registerActionHandler(
`${controllerName}:setAccountNameAndSelectAccount`,
this.setAccountNameAndSelectAccount.bind(this),
);

this.messagingSystem.registerActionHandler(
`${controllerName}:updateAccounts`,
this.updateAccounts.bind(this),
Expand Down
1 change: 1 addition & 0 deletions packages/accounts-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type {
AccountsControllerGetStateAction,
AccountsControllerSetSelectedAccountAction,
AccountsControllerSetAccountNameAction,
AccountsControllerSetAccountNameAndSelectAccountAction,
AccountsControllerListAccountsAction,
AccountsControllerListMultichainAccountsAction,
AccountsControllerUpdateAccountsAction,
Expand Down
Loading