Skip to content

Commit 207f2ea

Browse files
committed
feat: add mutex + wallet.status support
1 parent f6bfd02 commit 207f2ea

File tree

9 files changed

+312
-290
lines changed

9 files changed

+312
-290
lines changed

packages/multichain-account-service/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"@metamask/snaps-sdk": "^9.0.0",
5757
"@metamask/snaps-utils": "^11.0.0",
5858
"@metamask/superstruct": "^3.1.0",
59-
"@metamask/utils": "^11.4.2"
59+
"@metamask/utils": "^11.4.2",
60+
"async-mutex": "^0.5.0"
6061
},
6162
"devDependencies": {
6263
"@metamask/account-api": "^0.9.0",

packages/multichain-account-service/src/MultichainAccountGroup.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import { MultichainAccountGroup } from './MultichainAccountGroup';
1313
import { MultichainAccountWallet } from './MultichainAccountWallet';
1414
import type { MockAccountProvider } from './tests';
1515
import {
16-
getMultichainAccountServiceMessenger,
17-
getRootMessenger,
1816
MOCK_SNAP_ACCOUNT_2,
1917
MOCK_WALLET_1_BTC_P2TR_ACCOUNT,
2018
MOCK_WALLET_1_BTC_P2WPKH_ACCOUNT,

packages/multichain-account-service/src/MultichainAccountService.test.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -701,26 +701,50 @@ describe('MultichainAccountService', () => {
701701
});
702702

703703
it('returns true during alignWallets and false after completion', async () => {
704-
const { service } = setup({
704+
const { service, messenger } = setup({
705705
accounts: [MOCK_HD_ACCOUNT_1],
706706
});
707707

708+
expect(service.getIsAlignmentInProgress()).toBe(false);
709+
710+
const mockWalletStatusChange = jest.fn().mockImplementationOnce(() => {
711+
// When the alignment starts on any wallet, the service should report that
712+
// properly.
713+
expect(service.getIsAlignmentInProgress()).toBe(true);
714+
});
715+
716+
messenger.subscribe(
717+
'MultichainAccountService:walletStatusChange',
718+
mockWalletStatusChange,
719+
);
720+
708721
const alignmentPromise = service.alignWallets();
709-
expect(service.getIsAlignmentInProgress()).toBe(true);
710722

711723
await alignmentPromise;
712724
expect(service.getIsAlignmentInProgress()).toBe(false);
713725
});
714726

715727
it('returns true during alignWallet and false after completion', async () => {
716-
const { service } = setup({
728+
const { service, messenger } = setup({
717729
accounts: [MOCK_HD_ACCOUNT_1],
718730
});
719731

732+
expect(service.getIsAlignmentInProgress()).toBe(false);
733+
734+
const mockWalletStatusChange = jest.fn().mockImplementationOnce(() => {
735+
// When the alignment starts on any wallet, the service should report that
736+
// properly.
737+
expect(service.getIsAlignmentInProgress()).toBe(true);
738+
});
739+
740+
messenger.subscribe(
741+
'MultichainAccountService:walletStatusChange',
742+
mockWalletStatusChange,
743+
);
744+
720745
const alignmentPromise = service.alignWallet(
721746
MOCK_HD_KEYRING_1.metadata.id,
722747
);
723-
expect(service.getIsAlignmentInProgress()).toBe(true);
724748

725749
await alignmentPromise;
726750
expect(service.getIsAlignmentInProgress()).toBe(false);

packages/multichain-account-service/src/MultichainAccountService.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ export class MultichainAccountService {
151151
// (based on the accounts owned by each account providers).
152152
const wallet = new MultichainAccountWallet({
153153
entropySource,
154-
messenger: this.#messenger,
155154
providers: this.#providers,
156155
messenger: this.#messenger,
157156
});
@@ -185,7 +184,6 @@ export class MultichainAccountService {
185184
// That's a new wallet.
186185
wallet = new MultichainAccountWallet({
187186
entropySource: account.options.entropy.id,
188-
messenger: this.#messenger,
189187
providers: this.#providers,
190188
messenger: this.#messenger,
191189
});

packages/multichain-account-service/src/MultichainAccountWallet.test.ts

Lines changed: 20 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import type { InternalAccount } from '@metamask/keyring-internal-api';
1818
import { MultichainAccountWallet } from './MultichainAccountWallet';
1919
import type { MockAccountProvider } from './tests';
2020
import {
21-
getMultichainAccountServiceMessenger,
22-
getRootMessenger,
2321
MOCK_HD_ACCOUNT_1,
2422
MOCK_HD_KEYRING_1,
2523
MOCK_SNAP_ACCOUNT_2,
@@ -39,6 +37,7 @@ import type {
3937
AllowedEvents,
4038
MultichainAccountServiceActions,
4139
MultichainAccountServiceEvents,
40+
MultichainAccountServiceMessenger,
4241
} from './types';
4342

4443
function setup({
@@ -65,18 +64,21 @@ function setup({
6564
} = {}): {
6665
wallet: MultichainAccountWallet<Bip44Account<InternalAccount>>;
6766
providers: MockAccountProvider[];
67+
messenger: MultichainAccountServiceMessenger;
6868
} {
6969
providers ??= accounts.map((providerAccounts) => {
7070
return setupAccountProvider({ accounts: providerAccounts });
7171
});
7272

73+
const serviceMessenger = getMultichainAccountServiceMessenger(messenger);
74+
7375
const wallet = new MultichainAccountWallet<Bip44Account<InternalAccount>>({
7476
entropySource,
7577
providers,
76-
messenger: getMultichainAccountServiceMessenger(messenger),
78+
messenger: serviceMessenger,
7779
});
7880

79-
return { wallet, providers };
81+
return { wallet, providers, messenger: serviceMessenger };
8082
}
8183

8284
describe('MultichainAccountWallet', () => {
@@ -440,85 +442,28 @@ describe('MultichainAccountWallet', () => {
440442
});
441443

442444
it('returns true during alignment and false after completion', async () => {
443-
const { wallet } = setup();
444-
445-
// Start alignment (don't await yet)
446-
const alignmentPromise = wallet.alignGroups();
445+
const { wallet, messenger } = setup();
447446

448-
// Check if alignment is in progress
449-
expect(wallet.getIsAlignmentInProgress()).toBe(true);
450-
451-
// Wait for completion
452-
await alignmentPromise;
453-
454-
// Should be false after completion
455447
expect(wallet.getIsAlignmentInProgress()).toBe(false);
456-
});
457-
});
458-
459-
describe('concurrent alignment prevention', () => {
460-
it('prevents concurrent alignGroups calls', async () => {
461-
// Setup with EVM account in group 0, Sol account in group 1 (missing group 0)
462-
const mockEvmAccount = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1)
463-
.withEntropySource(MOCK_HD_KEYRING_1.metadata.id)
464-
.withGroupIndex(0)
465-
.get();
466-
const mockSolAccount = MockAccountBuilder.from(MOCK_SOL_ACCOUNT_1)
467-
.withEntropySource(MOCK_HD_KEYRING_1.metadata.id)
468-
.withGroupIndex(1)
469-
.get();
470-
const { wallet, providers } = setup({
471-
accounts: [[mockEvmAccount], [mockSolAccount]],
472-
});
473-
474-
// Make provider createAccounts slow to ensure concurrency
475-
providers[1].createAccounts.mockImplementation(
476-
() => new Promise((resolve) => setTimeout(() => resolve([]), 50)),
477-
);
478-
479-
// Start first alignment
480-
const firstAlignment = wallet.alignGroups();
481448

482-
// Start second alignment while first is still running
483-
const secondAlignment = wallet.alignGroups();
484-
485-
// Both should complete without error
486-
await Promise.all([firstAlignment, secondAlignment]);
487-
488-
// Provider should only be called once (not twice due to concurrency protection)
489-
expect(providers[1].createAccounts).toHaveBeenCalledTimes(1);
490-
});
491-
492-
it('prevents concurrent alignGroup calls', async () => {
493-
// Setup with EVM account in group 0, Sol account in group 1 (missing group 0)
494-
const mockEvmAccount = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1)
495-
.withEntropySource(MOCK_HD_KEYRING_1.metadata.id)
496-
.withGroupIndex(0)
497-
.get();
498-
const mockSolAccount = MockAccountBuilder.from(MOCK_SOL_ACCOUNT_1)
499-
.withEntropySource(MOCK_HD_KEYRING_1.metadata.id)
500-
.withGroupIndex(1)
501-
.get();
502-
const { wallet, providers } = setup({
503-
accounts: [[mockEvmAccount], [mockSolAccount]],
449+
const mockWalletStatusChange = jest.fn().mockImplementationOnce(() => {
450+
// Listen for wallet status change and check that the alignment is in progress.
451+
expect(wallet.getIsAlignmentInProgress()).toBe(true);
504452
});
505453

506-
// Make provider createAccounts slow to ensure concurrency
507-
providers[1].createAccounts.mockImplementation(
508-
() => new Promise((resolve) => setTimeout(() => resolve([]), 50)),
454+
messenger.subscribe(
455+
'MultichainAccountService:walletStatusChange',
456+
mockWalletStatusChange,
509457
);
510458

511-
// Start first alignment
512-
const firstAlignment = wallet.alignGroup(0);
513-
514-
// Start second alignment while first is still running
515-
const secondAlignment = wallet.alignGroup(0);
459+
// Start alignment (don't await yet)
460+
const alignmentPromise = wallet.alignGroups();
516461

517-
// Both should complete without error
518-
await Promise.all([firstAlignment, secondAlignment]);
462+
// Wait for completion
463+
await alignmentPromise;
519464

520-
// Provider should only be called once (not twice due to concurrency protection)
521-
expect(providers[1].createAccounts).toHaveBeenCalledTimes(1);
465+
// Should be false after completion
466+
expect(wallet.getIsAlignmentInProgress()).toBe(false);
522467
});
523468
});
524469

@@ -551,6 +496,7 @@ describe('MultichainAccountWallet', () => {
551496
jest.useFakeTimers();
552497
const discovery = wallet.discoverAndCreateAccounts();
553498
// Allow fast provider microtasks to run and advance maxGroupIndex first
499+
await Promise.resolve(); // Mutex lock.
554500
await Promise.resolve();
555501
await Promise.resolve();
556502
jest.advanceTimersByTime(100);

0 commit comments

Comments
 (0)