Skip to content

Conversation

@hmalik88
Copy link
Contributor

@hmalik88 hmalik88 commented Sep 18, 2025

Explanation

MultichainAccountService

  • Unified wallet creation into one flow (createMultichainAccountWallet) that handles import/restore/new vault.
  • Builds a single ServiceState index in one pass and passes state slices to wallets/groups (cuts repeated controller scans/calls).
  • Simplified init path and removed dead accountIdToContext mapping.

MultichainAccountWallet

  • init now consumes a pre-sliced wallet state (entropySource → groups → providerName → ids) instead of querying providers.
  • Emits clear events on group creation/updates; alignment orchestration uses provider state instead of full scans.

MultichainAccountGroup

  • init registers account IDs per provider and fills reverse maps; calls provider.addAccounts(ids) to keep providers in sync.
  • Added getAccountIds() for direct access to underlying IDs.
  • Improved partial‑failure reporting (aggregates provider errors by name).

BaseBip44AccountProvider

  • Added addAccounts(ids: string[]), enabling providers to track their own account ID lists.
  • getAccounts() paths rely on known IDs (plural lookups) rather than scanning the full controller list.

EvmAccountProvider

  • Switched from address‑based scans to ID‑based fetches (getAccount(s)) for create/discover (removes $O(n)$ scans).

Performance Analysis

n = total BIP-44 accounts in the AccountsController
p = number of providers (currently 4)
w = number of wallets (entropy sources)
g = total number of groups
e = number of created EVM accounts

When fully aligned $g = n / p$.
When accounts are not fully aligned then $g = max(f(p))$, where $f(p)$ is the number of accounts associated with a provider.

Consider two scenarios:

  1. State 1 -> State 2 transition, the user has unaligned groups after the transition.
  2. Already transitioned to State 2, the service is initialized after onboarding and every time the client is unlocked.

General formulas

For Scenario 2, the formulas are as follows:

Before this refactor, the number of loops can be represented $n * p * (1 + w + g)$, which with $p = 4$, becomes $n^2 + 4n(1 + w)$.

Before this refactor, the number of controller calls can be represented as $1 + w + g$, which with $p = 4$, becomes $1 + w + n/4$.

After this refactor, the number of loops can be represented by $n * p$, which with $p = 4$, becomes $4n$.

After this refactor, the number of calls is just $1$.

For Scenario 1, the formulas are entirely dependent on the breakdown of the number of accounts each provider has amongst the $n$ accounts, let's consider a scenario where Solana has $n/2$, Ethereum has $n/8$, Bitcoin has $n/4$ and Tron has $n/8$, the formulas would be as follows:

Before this refactor, the number of loops in the alignment process can be represented as $(p * g) + (n * e)$, which with $p=4$ and $g = n/2$, becomes $2n + 3n^2/8$. Therefore the number of loops for initialization + alignment in this scenario with $p = 4$ and $g = n/2$, becomes $(19/8)n^2 + (4w + 6)n$.

Before this refactor, the number of controller calls in the alignment process can be represented as $e$, which becomes $3n/8$. Therefore the number of controller calls for initialization + alignment in this scenario with $p = 4$, becomes $1 + w + 5n/8$.

After this refactor, the number of loops in the alignment process can be represented as $p * g$, which becomes $2n$. Therefore, the number of loops for initialization + alignment in this scenario with $p = 4$ and $g = n/2$, becomes $6n$.

After this refactor, the number of controller calls in the alignment process can be represented as $e$ which becomes $3n/8$. Therefore, the number of controller calls for initialization + alignment in this scenario with $p = 4$ and $g = n/2$, becomes $1 + 3n/8$.

In short, previous init performance for loops and controller calls was quadratic and linear, respectively. After, it is linear and constant.

Performance Charts

Below are charts that show performance (loops and controller calls) $n = 0$ -> $n = 256$ for Scenario 1 and 2 with $w = 2$, respectively:

MisalignedLoops MisalignedCalls AlignedLoops AlignedCalls

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, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

Note

BREAKING – performance refactor

  • Introduces ServiceState and state-driven init/update for Service, Wallet, and Group (no more on-demand scans or reverse context map)
  • Unifies wallet creation into createMultichainAccountWallet supporting import/create/restore; adds removeMultichainAccountWallet
  • Providers now init with account IDs, track IDs internally, expose alignAccounts, and fetch via getAccount(s) by ID (EVM uses deterministic ID)
  • MultichainAccountGroup/Wallet construct from provided state, add getAccountIds, and align all providers with aggregated error reporting and disabled-provider handling
  • Removes legacy sync flows and account context handlers; cleans up utils
  • Extensive test updates and changelog entries documenting breaking changes

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

accountsList,
);
// we cast here because we know that the accounts are BIP-44 compatible
return internalAccounts as Bip44Account<KeyringAccount>[];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Although the getAccounts's return type is (InternalAccount | undefined)[], we're sure to get back all the accounts we want since the accounts list will never be stale.

MultichainAccountWallet<Bip44Account<KeyringAccount>>
>;

readonly #accountIdToContext: Map<
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Decided to get rid of this mapping since it was only being used for handling the accountRemoved and accountAdded events, removing this gets rid of a large loop in the init function as well. If there's a particular need for this data at the client level, we can always add this back in.

) as HdKeyring[];

const mnemonicAsBytes = mnemonicPhraseToBytes(mnemonic);

Copy link

Choose a reason for hiding this comment

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

Wallet remains uninitialized after creation causing missing events

Medium Severity

When createMultichainAccountWallet creates a new wallet (via any of the three flows: import, create, or restore), wallet.init() is never called. The wallet remains with #initialized = false. However, #withLock in the wallet's finally block always sets #status = 'ready' unconditionally, creating an inconsistent state. This causes multichainAccountGroupCreated events to not be published when createMultichainAccountGroup is called later (line 479 checks #initialized). The service's init() method correctly calls wallet.init(), but the wallet creation flows do not.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed here: a2ea0f1

for (const account of accounts) {
this.accounts.add(account);
}
}
Copy link

Choose a reason for hiding this comment

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

Provider accounts not cleared during service re-initialization

Medium Severity

The BaseBip44AccountProvider.init() method only adds account IDs to the internal accounts Set without clearing it first. When MultichainAccountService.init() is called, it clears #wallets but then calls provider.init(state) which accumulates new IDs without removing old ones. If the service is re-initialized (e.g., after account changes), the providers retain stale account IDs. Subsequent calls to getAccount() for stale IDs would pass the has() check but then receive undefined from AccountsController:getAccount, causing the returned value to be undefined cast as Account.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

@hmalik88 hmalik88 Jan 9, 2026

Choose a reason for hiding this comment

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

We currently don't have a concept of deleting accounts, so this is a non-issue at the moment, AFAK. cc: @ccharly

);

this.#wallets.delete(wallet.id);
}
Copy link

Choose a reason for hiding this comment

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

Provider accounts Set not cleared on wallet removal

Medium Severity

When removeMultichainAccountWallet is called, the wallet is removed from the service's map and the account is removed via KeyringController:removeAccount, but the provider's internal accounts Set (in BaseBip44AccountProvider) retains the stale account IDs. Since providers are shared across all wallets and init() only adds to the Set without clearing it first, subsequent calls to provider.getAccounts() will attempt to fetch these non-existent account IDs from the AccountsController. The unsafe cast in getAccounts() would hide any undefined values returned for missing accounts, potentially causing downstream issues when code expects valid Account objects.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the point that this method is called, there is no entry created in the service state yet for this account that is being removed. The point is for it to be created in state when discoverAccounts is called, but since it will not be ever called before this method is, that is not an issue.

@hmalik88
Copy link
Contributor Author

hmalik88 commented Jan 9, 2026

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

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.0.0-preview-6a568504",
  "@metamask-previews/accounts-controller": "35.0.0-preview-6a568504",
  "@metamask-previews/address-book-controller": "7.0.1-preview-6a568504",
  "@metamask-previews/analytics-controller": "1.0.0-preview-6a568504",
  "@metamask-previews/announcement-controller": "8.0.0-preview-6a568504",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-6a568504",
  "@metamask-previews/approval-controller": "8.0.0-preview-6a568504",
  "@metamask-previews/assets-controllers": "95.0.0-preview-6a568504",
  "@metamask-previews/base-controller": "9.0.0-preview-6a568504",
  "@metamask-previews/bridge-controller": "64.4.0-preview-6a568504",
  "@metamask-previews/bridge-status-controller": "64.4.1-preview-6a568504",
  "@metamask-previews/build-utils": "3.0.4-preview-6a568504",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-6a568504",
  "@metamask-previews/claims-controller": "0.4.1-preview-6a568504",
  "@metamask-previews/composable-controller": "12.0.0-preview-6a568504",
  "@metamask-previews/controller-utils": "11.18.0-preview-6a568504",
  "@metamask-previews/core-backend": "5.0.0-preview-6a568504",
  "@metamask-previews/delegation-controller": "2.0.0-preview-6a568504",
  "@metamask-previews/earn-controller": "11.0.0-preview-6a568504",
  "@metamask-previews/eip-5792-middleware": "2.1.0-preview-6a568504",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-6a568504",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-6a568504",
  "@metamask-previews/ens-controller": "19.0.0-preview-6a568504",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-6a568504",
  "@metamask-previews/eth-block-tracker": "15.0.0-preview-6a568504",
  "@metamask-previews/eth-json-rpc-middleware": "22.0.1-preview-6a568504",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-6a568504",
  "@metamask-previews/foundryup": "1.0.1-preview-6a568504",
  "@metamask-previews/gas-fee-controller": "26.0.0-preview-6a568504",
  "@metamask-previews/gator-permissions-controller": "0.8.0-preview-6a568504",
  "@metamask-previews/json-rpc-engine": "10.2.0-preview-6a568504",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-6a568504",
  "@metamask-previews/keyring-controller": "25.0.0-preview-6a568504",
  "@metamask-previews/logging-controller": "7.0.1-preview-6a568504",
  "@metamask-previews/message-manager": "14.1.0-preview-6a568504",
  "@metamask-previews/messenger": "0.3.0-preview-6a568504",
  "@metamask-previews/multichain-account-service": "5.0.0-preview-6a568504",
  "@metamask-previews/multichain-api-middleware": "1.2.5-preview-6a568504",
  "@metamask-previews/multichain-network-controller": "3.0.0-preview-6a568504",
  "@metamask-previews/multichain-transactions-controller": "7.0.0-preview-6a568504",
  "@metamask-previews/name-controller": "9.0.0-preview-6a568504",
  "@metamask-previews/network-controller": "27.2.0-preview-6a568504",
  "@metamask-previews/network-enablement-controller": "4.0.0-preview-6a568504",
  "@metamask-previews/notification-services-controller": "21.0.0-preview-6a568504",
  "@metamask-previews/permission-controller": "12.2.0-preview-6a568504",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-6a568504",
  "@metamask-previews/phishing-controller": "16.1.0-preview-6a568504",
  "@metamask-previews/polling-controller": "16.0.0-preview-6a568504",
  "@metamask-previews/preferences-controller": "22.0.0-preview-6a568504",
  "@metamask-previews/profile-metrics-controller": "2.0.0-preview-6a568504",
  "@metamask-previews/profile-sync-controller": "27.0.0-preview-6a568504",
  "@metamask-previews/ramps-controller": "2.1.0-preview-6a568504",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-6a568504",
  "@metamask-previews/remote-feature-flag-controller": "4.0.0-preview-6a568504",
  "@metamask-previews/sample-controllers": "4.0.0-preview-6a568504",
  "@metamask-previews/seedless-onboarding-controller": "7.1.0-preview-6a568504",
  "@metamask-previews/selected-network-controller": "26.0.0-preview-6a568504",
  "@metamask-previews/shield-controller": "4.1.0-preview-6a568504",
  "@metamask-previews/signature-controller": "38.0.0-preview-6a568504",
  "@metamask-previews/storage-service": "0.0.1-preview-6a568504",
  "@metamask-previews/subscription-controller": "5.4.0-preview-6a568504",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-6a568504",
  "@metamask-previews/transaction-controller": "62.8.0-preview-6a568504",
  "@metamask-previews/transaction-pay-controller": "10.6.0-preview-6a568504",
  "@metamask-previews/user-operation-controller": "41.0.0-preview-6a568504"
}

@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.0.0-preview-fd4aea5a",
  "@metamask-previews/accounts-controller": "35.0.1-preview-fd4aea5a",
  "@metamask-previews/address-book-controller": "7.0.1-preview-fd4aea5a",
  "@metamask-previews/analytics-controller": "1.0.0-preview-fd4aea5a",
  "@metamask-previews/announcement-controller": "8.0.0-preview-fd4aea5a",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-fd4aea5a",
  "@metamask-previews/approval-controller": "8.0.0-preview-fd4aea5a",
  "@metamask-previews/assets-controller": "0.0.0-preview-fd4aea5a",
  "@metamask-previews/assets-controllers": "95.2.0-preview-fd4aea5a",
  "@metamask-previews/base-controller": "9.0.0-preview-fd4aea5a",
  "@metamask-previews/bridge-controller": "64.5.0-preview-fd4aea5a",
  "@metamask-previews/bridge-status-controller": "64.4.2-preview-fd4aea5a",
  "@metamask-previews/build-utils": "3.0.4-preview-fd4aea5a",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-fd4aea5a",
  "@metamask-previews/claims-controller": "0.4.1-preview-fd4aea5a",
  "@metamask-previews/composable-controller": "12.0.0-preview-fd4aea5a",
  "@metamask-previews/connectivity-controller": "0.0.0-preview-fd4aea5a",
  "@metamask-previews/controller-utils": "11.18.0-preview-fd4aea5a",
  "@metamask-previews/core-backend": "5.0.0-preview-fd4aea5a",
  "@metamask-previews/delegation-controller": "2.0.0-preview-fd4aea5a",
  "@metamask-previews/earn-controller": "11.0.0-preview-fd4aea5a",
  "@metamask-previews/eip-5792-middleware": "2.1.0-preview-fd4aea5a",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-fd4aea5a",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-fd4aea5a",
  "@metamask-previews/ens-controller": "19.0.1-preview-fd4aea5a",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-fd4aea5a",
  "@metamask-previews/eth-block-tracker": "15.0.0-preview-fd4aea5a",
  "@metamask-previews/eth-json-rpc-middleware": "22.0.1-preview-fd4aea5a",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-fd4aea5a",
  "@metamask-previews/foundryup": "1.0.1-preview-fd4aea5a",
  "@metamask-previews/gas-fee-controller": "26.0.1-preview-fd4aea5a",
  "@metamask-previews/gator-permissions-controller": "0.8.0-preview-fd4aea5a",
  "@metamask-previews/json-rpc-engine": "10.2.0-preview-fd4aea5a",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-fd4aea5a",
  "@metamask-previews/keyring-controller": "25.0.0-preview-fd4aea5a",
  "@metamask-previews/logging-controller": "7.0.1-preview-fd4aea5a",
  "@metamask-previews/message-manager": "14.1.0-preview-fd4aea5a",
  "@metamask-previews/messenger": "0.3.0-preview-fd4aea5a",
  "@metamask-previews/multichain-account-service": "5.0.0-preview-fd4aea5a",
  "@metamask-previews/multichain-api-middleware": "1.2.5-preview-fd4aea5a",
  "@metamask-previews/multichain-network-controller": "3.0.1-preview-fd4aea5a",
  "@metamask-previews/multichain-transactions-controller": "7.0.0-preview-fd4aea5a",
  "@metamask-previews/name-controller": "9.0.0-preview-fd4aea5a",
  "@metamask-previews/network-controller": "28.0.0-preview-fd4aea5a",
  "@metamask-previews/network-enablement-controller": "4.0.0-preview-fd4aea5a",
  "@metamask-previews/notification-services-controller": "21.0.0-preview-fd4aea5a",
  "@metamask-previews/permission-controller": "12.2.0-preview-fd4aea5a",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-fd4aea5a",
  "@metamask-previews/phishing-controller": "16.1.0-preview-fd4aea5a",
  "@metamask-previews/polling-controller": "16.0.1-preview-fd4aea5a",
  "@metamask-previews/preferences-controller": "22.0.0-preview-fd4aea5a",
  "@metamask-previews/profile-metrics-controller": "2.0.0-preview-fd4aea5a",
  "@metamask-previews/profile-sync-controller": "27.0.0-preview-fd4aea5a",
  "@metamask-previews/ramps-controller": "3.0.0-preview-fd4aea5a",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-fd4aea5a",
  "@metamask-previews/remote-feature-flag-controller": "4.0.0-preview-fd4aea5a",
  "@metamask-previews/sample-controllers": "4.0.1-preview-fd4aea5a",
  "@metamask-previews/seedless-onboarding-controller": "7.1.0-preview-fd4aea5a",
  "@metamask-previews/selected-network-controller": "26.0.1-preview-fd4aea5a",
  "@metamask-previews/shield-controller": "4.1.0-preview-fd4aea5a",
  "@metamask-previews/signature-controller": "38.0.1-preview-fd4aea5a",
  "@metamask-previews/storage-service": "0.0.1-preview-fd4aea5a",
  "@metamask-previews/subscription-controller": "5.4.0-preview-fd4aea5a",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-fd4aea5a",
  "@metamask-previews/transaction-controller": "62.9.1-preview-fd4aea5a",
  "@metamask-previews/transaction-pay-controller": "11.0.1-preview-fd4aea5a",
  "@metamask-previews/user-operation-controller": "41.0.1-preview-fd4aea5a"
}

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.

5 participants