Skip to content

perf!: Add accountIdByAddress to AccountsController state#7893

Open
hmalik88 wants to merge 25 commits intomainfrom
hm/mul-1449
Open

perf!: Add accountIdByAddress to AccountsController state#7893
hmalik88 wants to merge 25 commits intomainfrom
hm/mul-1449

Conversation

@hmalik88
Copy link
Contributor

@hmalik88 hmalik88 commented Feb 10, 2026

Explanation

Currently when doing an account lookup by address we're doing an O(n) operation in the clients, this has been improved to O(1) by adding an address -> account id mapping in state. The getAccountByAddress function now also takes advantage of this new piece of state to improve its lookup time.

Note: This will require a migration in the clients.

References

N/A

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Breaking state-shape change that requires client migrations and affects address-based account resolution paths. Risk is moderate due to widespread test/fixture updates and reliance on lowercasing addresses (noted as potentially incorrect for case-sensitive schemes).

Overview
BREAKING: Adds accountIdByAddress (lowercased address -> account id) to AccountsController state and derives/refreshes it during construction, updateAccounts, backup load, and keyring add/remove handling.

getAccountByAddress now uses this index instead of scanning listMultichainAccounts, and tests/fixtures across dependent packages are updated to include the new state shape (with accountIdByAddress marked non-persisted/non-logged in metadata).

Written by Cursor Bugbot for commit ceb8b26. This will update automatically on new commits. Configure here.

@hmalik88 hmalik88 requested a review from a team as a code owner February 10, 2026 16:13
@hmalik88 hmalik88 requested a review from a team as a code owner February 10, 2026 16:14
@hmalik88 hmalik88 changed the title perf: Add accountIdByAddress to AccountsController state perf!: Add accountIdByAddress to AccountsController state Feb 10, 2026
Comment on lines 442 to 444
const accountId =
this.state.internalAccounts.accountIdByAddress[address.toLowerCase()];
return accountId ? this.getAccount(accountId) : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this problem since quite some time now, but we should not lower-case addresses 😬 While EVM addresses are case-insensitive, non-EVM (at least Solana) are case-sensitives.

"Sometimes" (yes, we have this problem elsewhere 🥹) we're using the account.type to check if it's an EVM account type, and we either use checksummed addresses or lower-cased addresses.

Since we only have the address here, I wonder if we should check for the 0x prefix and lower-case only in this case?

Ideally we should just decide what is the format we want to use everywhere and we just stick to this, but we probably have some legacy code depending on non-sanitized addresses sometimes so...

Any opinion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just decided to continue to lowercase and left a comment around it as we discussed!


### Added

- **BREAKING:** Add `accountIdByAddress` mapping to state and update `getAccountByAddress` function ([#7893](https://github.com/MetaMask/core/pull/7893))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this breaking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It classifies as breaking since we have to write a migration in the clients no? getAccountByAddress calls would break.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so since this should be constructed inside the constructor therefore we shouldn't need a migration nor breaking change

@hmalik88 hmalik88 requested a review from a team as a code owner February 10, 2026 19:37
@hmalik88
Copy link
Contributor Author

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.1.1-preview-27e39dd44",
  "@metamask-previews/accounts-controller": "36.0.0-preview-27e39dd44",
  "@metamask-previews/address-book-controller": "7.0.1-preview-27e39dd44",
  "@metamask-previews/ai-controllers": "0.0.0-preview-27e39dd44",
  "@metamask-previews/analytics-controller": "1.0.0-preview-27e39dd44",
  "@metamask-previews/analytics-data-regulation-controller": "0.0.0-preview-27e39dd44",
  "@metamask-previews/announcement-controller": "8.0.0-preview-27e39dd44",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-27e39dd44",
  "@metamask-previews/approval-controller": "8.0.0-preview-27e39dd44",
  "@metamask-previews/assets-controller": "1.0.0-preview-27e39dd44",
  "@metamask-previews/assets-controllers": "99.3.2-preview-27e39dd44",
  "@metamask-previews/base-controller": "9.0.0-preview-27e39dd44",
  "@metamask-previews/bridge-controller": "66.1.0-preview-27e39dd44",
  "@metamask-previews/bridge-status-controller": "66.0.1-preview-27e39dd44",
  "@metamask-previews/build-utils": "3.0.4-preview-27e39dd44",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-27e39dd44",
  "@metamask-previews/claims-controller": "0.4.2-preview-27e39dd44",
  "@metamask-previews/composable-controller": "12.0.0-preview-27e39dd44",
  "@metamask-previews/connectivity-controller": "0.1.0-preview-27e39dd44",
  "@metamask-previews/controller-utils": "11.18.0-preview-27e39dd44",
  "@metamask-previews/core-backend": "5.1.1-preview-27e39dd44",
  "@metamask-previews/delegation-controller": "2.0.1-preview-27e39dd44",
  "@metamask-previews/earn-controller": "11.1.0-preview-27e39dd44",
  "@metamask-previews/eip-5792-middleware": "2.1.0-preview-27e39dd44",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-27e39dd44",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-27e39dd44",
  "@metamask-previews/ens-controller": "19.0.2-preview-27e39dd44",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-27e39dd44",
  "@metamask-previews/eth-block-tracker": "15.0.1-preview-27e39dd44",
  "@metamask-previews/eth-json-rpc-middleware": "23.1.0-preview-27e39dd44",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-27e39dd44",
  "@metamask-previews/foundryup": "1.0.1-preview-27e39dd44",
  "@metamask-previews/gas-fee-controller": "26.0.2-preview-27e39dd44",
  "@metamask-previews/gator-permissions-controller": "1.1.2-preview-27e39dd44",
  "@metamask-previews/json-rpc-engine": "10.2.2-preview-27e39dd44",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-27e39dd44",
  "@metamask-previews/keyring-controller": "25.1.0-preview-27e39dd44",
  "@metamask-previews/logging-controller": "7.0.1-preview-27e39dd44",
  "@metamask-previews/message-manager": "14.1.0-preview-27e39dd44",
  "@metamask-previews/messenger": "0.3.0-preview-27e39dd44",
  "@metamask-previews/multichain-account-service": "7.0.0-preview-27e39dd44",
  "@metamask-previews/multichain-api-middleware": "1.2.6-preview-27e39dd44",
  "@metamask-previews/multichain-network-controller": "3.0.3-preview-27e39dd44",
  "@metamask-previews/multichain-transactions-controller": "7.0.1-preview-27e39dd44",
  "@metamask-previews/name-controller": "9.0.0-preview-27e39dd44",
  "@metamask-previews/network-controller": "29.0.0-preview-27e39dd44",
  "@metamask-previews/network-enablement-controller": "4.1.0-preview-27e39dd44",
  "@metamask-previews/notification-services-controller": "22.0.0-preview-27e39dd44",
  "@metamask-previews/permission-controller": "12.2.0-preview-27e39dd44",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-27e39dd44",
  "@metamask-previews/perps-controller": "0.0.0-preview-27e39dd44",
  "@metamask-previews/phishing-controller": "16.2.0-preview-27e39dd44",
  "@metamask-previews/polling-controller": "16.0.2-preview-27e39dd44",
  "@metamask-previews/preferences-controller": "22.1.0-preview-27e39dd44",
  "@metamask-previews/profile-metrics-controller": "3.0.1-preview-27e39dd44",
  "@metamask-previews/profile-sync-controller": "27.1.0-preview-27e39dd44",
  "@metamask-previews/ramps-controller": "8.0.0-preview-27e39dd44",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-27e39dd44",
  "@metamask-previews/remote-feature-flag-controller": "4.0.0-preview-27e39dd44",
  "@metamask-previews/sample-controllers": "4.0.2-preview-27e39dd44",
  "@metamask-previews/seedless-onboarding-controller": "7.1.0-preview-27e39dd44",
  "@metamask-previews/selected-network-controller": "26.0.2-preview-27e39dd44",
  "@metamask-previews/shield-controller": "5.0.1-preview-27e39dd44",
  "@metamask-previews/signature-controller": "39.0.2-preview-27e39dd44",
  "@metamask-previews/storage-service": "1.0.0-preview-27e39dd44",
  "@metamask-previews/subscription-controller": "6.0.0-preview-27e39dd44",
  "@metamask-previews/transaction-controller": "62.17.0-preview-27e39dd44",
  "@metamask-previews/transaction-pay-controller": "12.2.0-preview-27e39dd44",
  "@metamask-previews/user-operation-controller": "41.0.2-preview-27e39dd44"
}

Copy link
Contributor

@mathieuartu mathieuartu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, added one possible readability improvement suggestion and a perf question

return this.listMultichainAccounts().find(
(account) => account.address.toLowerCase() === address.toLowerCase(),
);
const accountId = this.state.accountIdByAddress[address.toLowerCase()];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the toLowerCase() calls necessary? What would be the outcome if we dropped this?
It could be beneficial (although minimal) in terms of perf not to rely on this, also because we often recompute the same values. My reasoning also comes from seeing the FIXME comments hehe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I discussed this one with @ccharly and he suggested we continue with this pattern and take a look at how we handle addresses across the app in one fell swoop (I don't feel great about lowercasing either).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might have been, I can't recall exactly but the conclusion was let's just do this for now and come back and fix later. The only reason I can think is if things weren't being done properly in the clients.

const accounts = Object.values(accountsMap);
return accounts.reduce<Record<string, AccountId>>((acc, account) => {
acc[account.address.toLowerCase()] = account.id;
return acc;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case-sensitive addresses can collide in new map

Medium Severity

accountIdByAddress is built with address.toLowerCase() for every account, and getAccountByAddress also lowercases lookups. For case-sensitive non-EVM addresses, different accounts can collapse to one key, so lookups can return the wrong account ID or overwrite another entry in accountIdByAddress.

Additional Locations (2)

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

(account) => account.address.toLowerCase() === address.toLowerCase(),
);
const accountId = this.state.accountIdByAddress[address.toLowerCase()];
return accountId ? this.getAccount(accountId) : undefined;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address lookup no longer returns first match

Medium Severity

getAccountByAddress now relies on accountIdByAddress, which stores only one AccountId per lowercased address. When multiple accounts share the same normalized address, later inserts overwrite earlier ones, so lookups can return a different account than before instead of the first matching account.

Additional Locations (2)

Fix in Cursor Fix in Web

Comment on lines +228 to +233
accountIdByAddress: {
includeInStateLogs: false,
persist: false,
includeInDebugSnapshot: false,
usedInUi: true,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term plan is to remove the accounts-controller. How will this be translated to the other controllers?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants