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

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.

  • Core (Service)
    • Build ServiceState in one pass; pass slices to wallets/groups (wallet.init, group.init/update).
    • Replace address scans with ID lookups; add create wallet flow handling import | restore | create with validation and vault ops.
    • Remove reverse account mapping; simplify init; register new messenger actions (getAccounts, vault create/restore).
  • Wallet (MultichainAccountWallet)
    • State-driven construction of groups; new group creation initializes with EVM then background/awaited non‑EVM creation.
    • Non‑EVM creation aggregates errors; supports partial success; discovery builds groups from discovered IDs.
  • Group (MultichainAccountGroup)
    • New init/update and getAccountIds(); alignAccounts uses provider.alignAccounts, handles disabled providers, aggregates warnings, and emits updates.
  • Providers
    • BaseBip44AccountProvider: track IDs (init), fetch via AccountsController:getAccounts, alignAccounts returns IDs.
    • AccountProviderWrapper: add isDisabled and alignAccounts passthrough respecting enabled state.
    • EVM: deterministic ID via getUUIDFromAddressOfNormalAccount, use getAccount, update internal ID list; BTC/SOL/TRX add IDs on create/discover.
  • Types/Tests/Docs
    • Extend messenger types; overhaul tests for new flows and error handling; update CHANGELOG with breaking notes.

Written by Cursor Bugbot for commit 6bb050e. 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.

@cursor
Copy link

cursor bot commented Oct 23, 2025

Bug: Unsafe Cast in getAccounts() Method

The getAccounts() method performs an unsafe cast on the result from AccountsController:getAccounts. This action can return undefined values, but the method casts the array to Bip44Account<KeyringAccount>[] without filtering them. This can lead to runtime errors, like "Cannot read property 'id' of undefined," when consumers (e.g., getAccount()) access properties on these undefined entries.

Fix in Cursor Fix in Web

@cursor
Copy link

cursor bot commented Oct 23, 2025

Bug: Duplicate Account IDs in Multichain Provider

The BaseBip44AccountProvider.addAccounts method doesn't prevent duplicate account IDs, allowing MultichainAccountGroup.init or update to add the same IDs multiple times. This results in providers holding duplicate account IDs, which may cause unexpected behavior in methods relying on this internal list.

Additional Locations (1)

Fix in Cursor Fix in Web

@cursor
Copy link

cursor bot commented Oct 23, 2025

Bug: Async Provider Failure Leaves Wallet Inconsistent

In createMultichainAccountGroup, when not awaiting all providers, the MultichainAccountGroup is created and added to the wallet's map synchronously. However, the EVM provider's account creation and group initialization are asynchronous. If the EVM provider fails, the group is left in an uninitialized or incomplete state in the map, leading to inconsistent wallet state and unexpected behavior for callers.

Fix in Cursor Fix in Web

@cursor
Copy link

cursor bot commented Oct 23, 2025

Bug: Error Handling Anti-Pattern in alignAccounts

The alignAccounts method uses Promise.reject(new Error('Already aligned')) for control flow, which is an anti-pattern. This relies on fragile string matching to distinguish between actual errors and control flow, risking incorrect failure reporting if the message changes.

Fix in Cursor Fix in Web

}
return Promise.resolve();

return accounts;
Copy link

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:

  1. Maps are cleared: this.#providerToAccounts.clear() and this.#accountToProvider.clear()
  2. Providers are called to align accounts
  3. Only providers that return accounts > 0 repopulate the maps (line 264-268: if (accounts.length > 0))
  4. 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

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.

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...');
Copy link

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.

Fix in Cursor Fix in Web

groupIndex: number;
}): Promise<[boolean, Account['id'][]]> {
const accounts = await this.createAccounts({
entropySource,
Copy link

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.

Fix in Cursor Fix in Web

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