-
-
Notifications
You must be signed in to change notification settings - Fork 253
refactor(multichain-account-service): Improved performance across package classes and improved error messages #6654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…to wallets and groups
…tead of getAccountByAddress which iterates through the whole of internal accounts in the AccountsController
| accountsList, | ||
| ); | ||
| // we cast here because we know that the accounts are BIP-44 compatible | ||
| return internalAccounts as Bip44Account<KeyringAccount>[]; |
There was a problem hiding this comment.
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.
…accountAdded and accountRemoved handling, it is dead code
| MultichainAccountWallet<Bip44Account<KeyringAccount>> | ||
| >; | ||
|
|
||
| readonly #accountIdToContext: Map< |
There was a problem hiding this comment.
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.
…handle createNewVaultAndKeychain and createNewVaultAndRestore code paths
…s, remove redundant state assignment, use assert to ensure wallet existence after creation
…ble with new changes
Bug: Unsafe Cast in
|
Bug: Duplicate Account IDs in Multichain ProviderThe Additional Locations (1) |
Bug: Async Provider Failure Leaves Wallet InconsistentIn |
Bug: Error Handling Anti-Pattern in
|
…hen it is disabled, fixed tests
| } | ||
| return Promise.resolve(); | ||
|
|
||
| return accounts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug
In the alignAccounts method, the internal maps #providerToAccounts and #accountToProvider are cleared at the start (lines 249-250), but if a provider returns 0 accounts (either because it's disabled or has no missing accounts), those maps will not be repopulated for that provider. This causes data loss.
The issue occurs because:
- Maps are cleared:
this.#providerToAccounts.clear()andthis.#accountToProvider.clear() - Providers are called to align accounts
- Only providers that return accounts > 0 repopulate the maps (line 264-268:
if (accounts.length > 0)) - The
update(groupState)at line 291 is called, but providers with 0 accounts won't be in the new state
This means if a provider already has accounts aligned but alignAccounts() is called again, and the provider correctly returns an empty array (because no missing accounts), those existing accounts will be lost from the internal state. The accounts still exist in the AccountsController, but the group can no longer find them because the reverse-lookup maps have been cleared and not repopulated.
The correct approach would be to either:
- Not clear the maps and instead update them incrementally, OR
- Ensure all providers repopulate their entries even when returning empty arrays, OR
- Reconstruct the full state including existing accounts before clearing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong assessment of the logic. The alignAccounts method will call createAccounts and expect it to return an up-to-date list of the provider's accounts, hence why we reuse those values to set the maps.
| this.#log('Synchronized'); | ||
| update | ||
| ? this.#log('Finished updating group state...') | ||
| : this.#log('Finished initializing group state...'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Stale Account Data Persists After Updates
The init method doesn't clear the #providerToAccounts and #accountToProvider maps when called with update = true. When update() is invoked with partial state (e.g., only one provider's accounts), old entries for other providers remain in the maps, causing stale data accumulation. This is inconsistent with alignAccounts which clears the maps before repopulation.
| groupIndex: number; | ||
| }): Promise<[boolean, Account['id'][]]> { | ||
| const accounts = await this.createAccounts({ | ||
| entropySource, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Provider Status Mismatch Blocks Account Creation
The alignAccounts method returns [true, accountIds] where the first element should indicate if the provider is disabled. Returning true means all non-wrapped providers are incorrectly treated as disabled during alignment in MultichainAccountGroup.alignAccounts, preventing account creation. Should return [false, accountIds] to indicate the provider is enabled.
Explanation
MultichainAccountServicecreateMultichainAccountWallet) that handles import/restore/new vault.ServiceStateindex in one pass and passes state slices to wallets/groups (cuts repeated controller scans/calls).initpath and removed deadaccountIdToContextmapping.MultichainAccountWalletinitnow consumes a pre-sliced wallet state (entropySource → groups → providerName → ids) instead of querying providers.MultichainAccountGroupinitregisters account IDs per provider and fills reverse maps; callsprovider.addAccounts(ids)to keep providers in sync.getAccountIds()for direct access to underlying IDs.BaseBip44AccountProvideraddAccounts(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.EvmAccountProvidergetAccount(s)) for create/discover (removesPerformance Analysis
When fully aligned$g = n / p$ .$g = max(f(p))$ , where $f(p)$ is the number of accounts associated with a provider.
When accounts are not fully aligned then
Consider two scenarios:
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
initperformance 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:
References
N/A
Checklist
Note
Major perf refactor: build a single state index for init, unify wallet creation (import/restore/create), track provider account IDs, and improve alignment with partial failures and disabled providers.
ServiceStatein one pass; pass slices to wallets/groups (wallet.init,group.init/update).import | restore | createwith validation and vault ops.init; register new messenger actions (getAccounts, vault create/restore).MultichainAccountWallet)MultichainAccountGroup)init/updateandgetAccountIds();alignAccountsusesprovider.alignAccounts, handles disabled providers, aggregates warnings, and emits updates.BaseBip44AccountProvider: track IDs (init), fetch viaAccountsController:getAccounts,alignAccountsreturns IDs.AccountProviderWrapper: addisDisabledandalignAccountspassthrough respecting enabled state.getUUIDFromAddressOfNormalAccount, usegetAccount, update internal ID list; BTC/SOL/TRX add IDs on create/discover.Written by Cursor Bugbot for commit 6bb050e. This will update automatically on new commits. Configure here.