Skip to content
Open
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]

### Fixed

- Prevent `listMultichainAccounts` from throwing a `TypeError` when an internal account has an undefined `scopes` field ([#41962](https://github.com/MetaMask/metamask-extension/issues/41962))

## [39.0.5]

### Changed
Expand Down
41 changes: 41 additions & 0 deletions packages/accounts-controller/src/AccountsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3896,6 +3896,47 @@ describe('AccountsController', () => {
accountsController.listMultichainAccounts(invalidCaip2),
).toThrow(`Invalid CAIP-2 chain ID: ${invalidCaip2}`);
});

it('does not throw when an account has an undefined `scopes` field and excludes it from chain-filtered results', () => {
// Regression test: a legacy or partially-migrated internal account can be
// persisted without a `scopes` field (its runtime value is `undefined`),
// even though the type declares it required. Filtering by a chain ID must
// not throw for such an account; it declares no scopes, so it matches no
// chain and is simply excluded.
const mockAccountWithoutScopes = {
...createMockInternalAccount({
id: 'mock-account-without-scopes-id',
address: '0x9999999999999999999999999999999999999999',
name: 'Legacy Account',
}),
scopes: undefined,
} as unknown as InternalAccount;

const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
[mockAccount.id]: mockAccount,
[mockAccountWithoutScopes.id]: mockAccountWithoutScopes,
},
selectedAccount: mockAccount.id,
},
accountIdByAddress: {
[mockAccount.address]: mockAccount.id,
[mockAccountWithoutScopes.address]: mockAccountWithoutScopes.id,
},
},
});

expect(() =>
accountsController.listMultichainAccounts(BtcScope.Mainnet),
).not.toThrow();
// The well-formed account is still returned for its scope; the account
// with undefined `scopes` is excluded rather than throwing.
expect(
accountsController.listMultichainAccounts(EthScope.Eoa),
).toStrictEqual([mockAccount]);
});
});

describe('setSelectedAccount', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/accounts-controller/src/AccountsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,12 @@ export class AccountsController extends BaseController<
throw new Error(`Invalid CAIP-2 chain ID: ${String(chainId)}`);
}

return accounts.filter((account) =>
isScopeEqualToAny(chainId, account.scopes),
return accounts.filter(
(account) =>
// A legacy or partially-migrated account can be persisted without a
// `scopes` field, so guard against a non-array value before matching.
Array.isArray(account.scopes) &&
isScopeEqualToAny(chainId, account.scopes),
);
}

Expand Down