Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5207,12 +5207,13 @@ export default class MetamaskController extends EventEmitter {
}

await this.accountsController.updateAccounts();
///: BEGIN:ONLY_INCLUDE_IF(multichain)

// Init multichain accounts after creating internal accounts.
await this.multichainAccountService.init();
///: END:ONLY_INCLUDE_IF

// Force account-tree refresh after all accounts have been updated.
this.accountTreeController.init();

// TODO: Move this logic to the SnapKeyring directly.
// Forward selected accounts to the Snap keyring, so each Snaps can fetch those accounts.
await this.forwardSelectedAccountGroupToSnapKeyring(
Expand All @@ -5221,12 +5222,23 @@ export default class MetamaskController extends EventEmitter {
);

if (this.isMultichainAccountsFeatureState2Enabled()) {
// This allows to create missing accounts if new account providers have been added.
const resyncAndAlignAccounts = async () => {
// READ THIS CAREFULLY:
// There is/was a bug with Snap accounts that can be desynchronized (Solana). To
// automatically "fix" this corrupted state, we run this method which will re-sync
// MetaMask accounts and Snap accounts upon login.
// BUG: https://github.com/MetaMask/metamask-extension/issues/37228
await this.multichainAccountService.resyncAccounts();

// This allows to create missing accounts if new account providers have been added.
await this.multichainAccountService.alignWallets();
};

// FIXME: We might wanna run discovery + alignment asynchronously here, like we do
// for mobile, but for now, this easy fix should cover new provider accounts.
// for mobile.
// NOTE: We run this asynchronously on purpose, see FIXME^.
// eslint-disable-next-line no-void
void this.multichainAccountService.alignWallets();
void resyncAndAlignAccounts();
Copy link

Choose a reason for hiding this comment

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

Bug: Unconditional multichain service calls cause runtime errors

The conditional compilation directives around multichainAccountService.init() were removed, but multichainAccountService is only defined in builds with the multichain flag enabled. This causes the code to attempt calling methods on undefined in non-multichain builds, resulting in runtime errors. The same issue affects the resyncAndAlignAccounts function which calls resyncAccounts() and alignWallets() on the potentially undefined service.

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.

I actually removed it cause we were already using the service without the codefence. Anyway, this code fence should be entirely remove at some point.

}
}

Expand Down
42 changes: 42 additions & 0 deletions app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,20 @@ function* ulidGenerator(ulids = mockULIDs) {
throw new Error('should not be called after exhausting provided IDs');
}

/**
* Utility function that waits for all pending promises to be resolved.
* This is necessary when testing asynchronous execution flows that are
* initiated by synchronous calls.
*
* @returns A promise that resolves when all pending promises are completed.
*/
async function waitForAllPromises() {
// Wait for next tick to flush all pending promises. It's requires since
// we are testing some asynchronous execution flows that are started by
// synchronous calls.
await new Promise(process.nextTick);
}

/**
* Generate mock patches for a complete state replacement.
*
Expand Down Expand Up @@ -728,6 +742,34 @@ describe('MetaMaskController', () => {
});
});

describe('#submitPasswordOrEncryptionKey', () => {
const password = 'a-fake-password';

it('should call resyncAccounts and alignWallets asynchronously when submitPasswordOrEncryptionKey is called', async () => {
const mockAlignWallets = jest.fn();
const mockResyncAccounts = jest.fn();

// We only trigger this behavior when the feature flag is enabled.
jest
.spyOn(metamaskController, 'isMultichainAccountsFeatureState2Enabled')
.mockReturnValue(true);

metamaskController.multichainAccountService = {
init: jest.fn(),
resyncAccounts: mockResyncAccounts,
alignWallets: mockAlignWallets,
};

await metamaskController.createNewVaultAndRestore(password, TEST_SEED);
await metamaskController.submitPasswordOrEncryptionKey({ password });

await waitForAllPromises();

expect(mockResyncAccounts).toHaveBeenCalled();
expect(mockAlignWallets).toHaveBeenCalled();
});
Copy link

Choose a reason for hiding this comment

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

Bug: Race condition in async test verification

The test uses waitForAllPromises() which only waits for one process.nextTick, but the resyncAndAlignAccounts function contains two sequential await statements. This creates a race condition where the test assertions may execute before both resyncAccounts and alignWallets complete, causing intermittent test failures. The fire-and-forget pattern with void means the function execution isn't guaranteed to finish before the assertions run.

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.

That's ok for now, I'll keep this simple.

});

describe('setLocked', () => {
it('should lock KeyringController', async () => {
await metamaskController.createNewVaultAndKeychain('password');
Expand Down
Loading