Skip to content

Conversation

ccharly
Copy link
Contributor

@ccharly ccharly commented Jul 22, 2025

Explanation

Re-sync multichain accounts and wallets upon AccountsController events.

References

N/A

Changelog

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

@ccharly
Copy link
Contributor Author

ccharly commented Jul 23, 2025

@metamaskbot publish-preview

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": "0.5.0-preview-21067ca7",
  "@metamask-previews/accounts-controller": "31.0.0-preview-21067ca7",
  "@metamask-previews/address-book-controller": "6.1.1-preview-21067ca7",
  "@metamask-previews/announcement-controller": "7.0.3-preview-21067ca7",
  "@metamask-previews/app-metadata-controller": "1.0.0-preview-21067ca7",
  "@metamask-previews/approval-controller": "7.1.3-preview-21067ca7",
  "@metamask-previews/assets-controllers": "72.0.0-preview-21067ca7",
  "@metamask-previews/base-controller": "8.0.1-preview-21067ca7",
  "@metamask-previews/bridge-controller": "36.1.0-preview-21067ca7",
  "@metamask-previews/bridge-status-controller": "36.0.0-preview-21067ca7",
  "@metamask-previews/build-utils": "3.0.3-preview-21067ca7",
  "@metamask-previews/chain-agnostic-permission": "1.0.0-preview-21067ca7",
  "@metamask-previews/composable-controller": "11.0.0-preview-21067ca7",
  "@metamask-previews/controller-utils": "11.11.0-preview-21067ca7",
  "@metamask-previews/delegation-controller": "0.5.0-preview-21067ca7",
  "@metamask-previews/earn-controller": "3.0.0-preview-21067ca7",
  "@metamask-previews/eip1193-permission-middleware": "1.0.0-preview-21067ca7",
  "@metamask-previews/ens-controller": "17.0.1-preview-21067ca7",
  "@metamask-previews/error-reporting-service": "2.0.0-preview-21067ca7",
  "@metamask-previews/eth-json-rpc-provider": "4.1.8-preview-21067ca7",
  "@metamask-previews/foundryup": "1.0.0-preview-21067ca7",
  "@metamask-previews/gas-fee-controller": "24.0.0-preview-21067ca7",
  "@metamask-previews/json-rpc-engine": "10.0.3-preview-21067ca7",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.7-preview-21067ca7",
  "@metamask-previews/keyring-controller": "22.1.0-preview-21067ca7",
  "@metamask-previews/logging-controller": "6.0.4-preview-21067ca7",
  "@metamask-previews/message-manager": "12.0.2-preview-21067ca7",
  "@metamask-previews/messenger": "0.0.0-preview-21067ca7",
  "@metamask-previews/multichain-account-service": "0.0.0-preview-21067ca7",
  "@metamask-previews/multichain-api-middleware": "1.0.0-preview-21067ca7",
  "@metamask-previews/multichain-network-controller": "0.10.0-preview-21067ca7",
  "@metamask-previews/multichain-transactions-controller": "3.0.0-preview-21067ca7",
  "@metamask-previews/name-controller": "8.0.3-preview-21067ca7",
  "@metamask-previews/network-controller": "24.0.1-preview-21067ca7",
  "@metamask-previews/notification-services-controller": "14.0.0-preview-21067ca7",
  "@metamask-previews/permission-controller": "11.0.6-preview-21067ca7",
  "@metamask-previews/permission-log-controller": "4.0.0-preview-21067ca7",
  "@metamask-previews/phishing-controller": "13.1.0-preview-21067ca7",
  "@metamask-previews/polling-controller": "14.0.0-preview-21067ca7",
  "@metamask-previews/preferences-controller": "18.4.1-preview-21067ca7",
  "@metamask-previews/profile-sync-controller": "21.0.0-preview-21067ca7",
  "@metamask-previews/rate-limit-controller": "6.0.3-preview-21067ca7",
  "@metamask-previews/remote-feature-flag-controller": "1.6.0-preview-21067ca7",
  "@metamask-previews/sample-controllers": "1.0.0-preview-21067ca7",
  "@metamask-previews/seedless-onboarding-controller": "2.4.0-preview-21067ca7",
  "@metamask-previews/selected-network-controller": "23.0.0-preview-21067ca7",
  "@metamask-previews/signature-controller": "31.0.1-preview-21067ca7",
  "@metamask-previews/token-search-discovery-controller": "3.3.0-preview-21067ca7",
  "@metamask-previews/transaction-controller": "58.1.1-preview-21067ca7",
  "@metamask-previews/user-operation-controller": "37.0.0-preview-21067ca7"
}

@ccharly ccharly marked this pull request as ready for review July 23, 2025 13:43
@ccharly ccharly requested review from a team as code owners July 23, 2025 13:43
Copy link

socket-security bot commented Jul 23, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​metamask/​account-api@​0.2.0 ⏵ 0.3.074 +110072 +193 +2100

View full report

@ccharly ccharly requested a review from a team as a code owner July 23, 2025 14:01
Copy link
Member

@gantunesr gantunesr left a comment

Choose a reason for hiding this comment

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

Overall it looks good. Left a question and I still need to see the rest of the code to understand better these changes.

Comment on lines 141 to 143
// This new account might be a new multichain account, and the wallet might not
// know it yet, so we need to force-sync here.
wallet.sync();
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if I follow what is happening here. If it is a new account then the wallet should not now it, I don't understand why it is a "might".

And the sync operation seems quite expensive to be executed for every new account added, at least the second part of it,

for (const [groupIndex, multichainAccount] of this.#accounts.entries()) {
  multichainAccount.sync();

  // Clean up old multichain accounts.
  if (!multichainAccount.hasAccounts()) {
    this.#accounts.delete(groupIndex);
  }
}

Copy link
Contributor Author

@ccharly ccharly Jul 24, 2025

Choose a reason for hiding this comment

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

For the moment, we don't control the account creation through the multichain account "SDK".

So you might have the following scenario:

  • You have "Wallet 1"
    • It has 1 "Multichain Account 1" (index 0)
  • You now create a new EVM account for the next index (index 1)
  • This will trigger a :accountAdded event
  • Currently "Wallet 1" lives in memory and it does not know about this new account (index 1) yet
  • Thus, the service sync the appropriate wallet (by checking the options.entropy.id):
    • To let it know that a new account got added
      • So the wallet can check all accounts (from its providers) and create (a MultichainAccount instance) for the missing "Multichain Account 2" (index 1)

I initially had another idea where AccountProvider would implement a sort pub/sub pattern to let the MultichainAccountWallet or MultichainAccount instances that something got added:

But the implementation felt more complex, and I opted for the "manual sync" + controller events instead (at least, for the moment).

And the sync operation seems quite expensive to be executed for every new account added

I'd say, it's ok for the moment. We can always change this if needed (it won't change the public API part). Also, we might not use events anymore once we introduce operations to create "a multichain account" entirely through the SDK/account providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That being said, I can maybe improve this 🤔

Given that we only filter for Bip44Account, we can do this:

// `account` is a `Bip44Account`.
const wallet = this.#wallets.get(
  toMultichainAccountWalletId(account.options.entropy.id),
);
if (wallet) {
  let multichainAccount = wallet.getMultichainAccount(
    account.options.entropy.groupIndex
  );
  if (multichainAccount) {
    // This new account is part of an existing multichain account, let it
    // re-sync with its providers.
    multichainAccount.sync();
  } else {
    // This new account is a new multichain account, let the wallet know
    // it has to re-sync with its providers.
    wallet.sync();
    
    multichainAccount = wallet.getMultichainAccount(
      account.options.entropy.groupIndex,
    );
  }
  
  if (multichainAccount) {
    // Same here, this account should have been already grouped in that
    // multichain account.
    this.#reverse.set(account.id, {
      wallet,
      multichainAccount,
    });
  }
}

Thus we avoid doing a wallet.sync unnecessarily.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed on the sync method feeling like overkill for a single account added. Wondering if there's a more surgical way of syncing this new account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, delegating the synchronization on the wallet and multichain accounts themselves might be better indeed.

I do have a draft PR for this, but this makes the responsibility of the "account providers". So for now, I'd just keep the sync method until we find a better pattern for this. That being said, I do agree that syncing the entire wallet for 1 account can be avoided, so I'll change the logic for now.

if (found) {
const { wallet } = found;

wallet.sync();
Copy link
Contributor

Choose a reason for hiding this comment

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

Same sentiment as above re: adding an account.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did refactor this, lemme know what you think. (I still believe we could do it in a more surgical way, but that should be ok for now).

return new MockAccountBuilder(account);
}

withId(id: InternalAccount['id']) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be Bip44Account<InternalAccount>['id']?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we're dealing with InternalAccount in the tests, I'll keep this as InternalAccount['id'] (Though, I made the change for other InternalAccount['id'], cause I also think it's better that way!) Thanks

@ccharly ccharly enabled auto-merge (squash) July 24, 2025 15:14
@ccharly ccharly merged commit 6bf9844 into main Jul 24, 2025
219 checks passed
@ccharly ccharly deleted the feat/multichain-accounts-on-account-events branch July 24, 2025 15:20
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.

3 participants