-
-
Notifications
You must be signed in to change notification settings - Fork 252
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
packages/multichain-account-service/src/MultichainAccountGroup.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/MultichainAccountGroup.ts
Outdated
Show resolved
Hide resolved
| return created; | ||
| } | ||
| return Promise.resolve(); | ||
| return Promise.reject(new Error('Already aligned')); |
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.
I would not use an error message in that case. Maybe we can re-use what you did in the EvmAccountProvider.#createAccount last time, like returning a boolean to indicate if we created accounts or not?
Like const [didCreate, accounts] = .... WDYT?
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.
Do you mean changing the provider's createAccounts return type or returning the tuple in the promise in Promise.allSettled? I found this method easier because then we're returning a tuple just for this one use case.
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.
Well nope, indeed we don't want to change the signature of createAccounts 🤔
Given the current logic of having to "remove" accounts from the provider during alignment, I think we could introduce a BaseBip44AccountProvider.alignAccounts({ entropySource, groupIndex }) method maybe...
This way, for the AccountProviderWrapper, we can automatically "remove" accounts from it when disabled, and for all other cases, we can just return existing and new (missing) accounts from it.
We would always re-use the returned value to update #providerToAccounts accounts so we always make sure to have an "in-sync" list of accounts with the provider.
WDYT?
NOTE: I think we could go with my small PR here:
- refactor(multichain-account-service): add
Bip44AccountProviderinterface #6959
This way, we can add thealignAccountson the new type interface I've introduced.
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.
Removed the promise rejection to just return an empty array. I applied the changes you had in your PR to the interface as well. I don't know if we need alignAccounts on the provider level since it essentially becomes an extra wrapper on createAccounts since we have to check at the group level anyway for if a provider is disabled to clear the group's state.
packages/multichain-account-service/src/MultichainAccountGroup.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/MultichainAccountService.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/MultichainAccountService.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts
Outdated
Show resolved
Hide resolved
packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts
Outdated
Show resolved
Hide resolved
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
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
Refactors the multichain account service to a state-driven init with unified wallet creation flows, ID-based provider lookups, improved alignment/disabled-provider handling, and updated messaging/types and tests.
ServiceStateonce and pass slices toMultichainAccountWallet.initandMultichainAccountGroup.init/update(removes repeated controller scans).sync,getAccountContext,#handleOnAccount{Added,Removed}.createMultichainAccountWalletto handleimport,create(new vault), andrestore; add validation; wire new KeyringController actions.GroupState/WalletState; groups maintain provider↔account-ID maps; addgetAccountIds.BaseBip44AccountProvider: track IDs viaaddAccounts;getAccountsfetches by IDs (AccountsController:getAccounts); addclearAccountsList.EvmAccountProvider: use deterministic ID (getUUIDFromAddressOfNormalAccount) andAccountsController:getAccount; optimize discovery/create; retain retry/timeout.AccountProviderWrapper: add disable semantics, clearing accounts on disable; exposeisDisabled.AccountsController:getAccounts,KeyringController:{createNewVaultAndKeychain,createNewVaultAndRestore}.Written by Cursor Bugbot for commit 37f79b6. This will update automatically on new commits. Configure here.