@@ -16,9 +16,60 @@ import type {
1616 NavigationContainerRef,
1717 ParamListBase,
1818} from '@react-navigation/native';
19+ import { AccountGroupType, AccountWalletType } from '@metamask/account-api';
1920
2021const mockCreateWallet = jest.fn().mockResolvedValue(undefined);
22+ const mockCreateAccountGroups = jest.fn().mockResolvedValue([]);
2123const mockImportAccount = jest.fn().mockResolvedValue(undefined);
24+ const mockIsUnlocked = jest.fn(() => true);
25+ const mockSubmitPassword = jest.fn().mockResolvedValue(undefined);
26+ const FIXTURE_WALLET_ID = 'entropy:keyring-1' as const;
27+
28+ function mockEvmAccount(id: string, address: string, name: string) {
29+ return {
30+ id,
31+ address,
32+ type: 'eip155:eoa' as const,
33+ options: {},
34+ scopes: ['eip155:1' as const],
35+ methods: [],
36+ metadata: {
37+ name,
38+ importTime: 0,
39+ keyring: { type: 'HD Key Tree' },
40+ },
41+ };
42+ }
43+
44+ function mockEntropyGroup(
45+ groupIndex: number,
46+ accountIds: [string, ...string[]],
47+ name: string,
48+ ): {
49+ type: AccountGroupType.MultichainAccount;
50+ id: `${typeof FIXTURE_WALLET_ID}/${number}`;
51+ accounts: [string, ...string[]];
52+ metadata: {
53+ name: string;
54+ pinned: boolean;
55+ hidden: boolean;
56+ lastSelected: number;
57+ entropy: { groupIndex: number };
58+ };
59+ } {
60+ return {
61+ type: AccountGroupType.MultichainAccount,
62+ id: `${FIXTURE_WALLET_ID}/${groupIndex}` as const,
63+ accounts: accountIds,
64+ metadata: {
65+ name,
66+ pinned: false,
67+ hidden: false,
68+ lastSelected: 0,
69+ entropy: { groupIndex },
70+ },
71+ };
72+ }
2273
2374jest.mock('../Engine', () => ({
2475 context: {
@@ -48,9 +99,13 @@ jest.mock('../Engine', () => ({
4899 MultichainAccountService: {
49100 createMultichainAccountWallet: (...args: unknown[]) =>
50101 mockCreateWallet(...args),
102+ createMultichainAccountGroups: (...args: unknown[]) =>
103+ mockCreateAccountGroups(...args),
51104 init: jest.fn().mockResolvedValue(undefined),
52105 },
53106 KeyringController: {
107+ isUnlocked: () => mockIsUnlocked(),
108+ submitPassword: (password: string) => mockSubmitPassword(password),
54109 importAccountWithStrategy: (...args: unknown[]) =>
55110 mockImportAccount(...(args as [string, string[]])),
56111 },
@@ -220,6 +275,13 @@ function installFiberHook(rootFiber: FiberNode) {
220275 };
221276}
222277
278+ function resetMockAccountState() {
279+ Engine.context.AccountsController.state.internalAccounts.accounts = {
280+ a1: mockEvmAccount('a1', '0xABC', 'Account 1'),
281+ };
282+ Engine.context.AccountTreeController.state.accountTree = { wallets: {} };
283+ }
284+
223285// ─── Fiber tree helper tests ────────────────────────────────────────────────
224286
225287describe('walkFiber', () => {
@@ -766,8 +828,17 @@ describe('AgenticService.install', () => {
766828 describe('setupWallet', () => {
767829 beforeEach(() => {
768830 mockCreateWallet.mockClear();
831+ mockCreateAccountGroups.mockReset();
832+ mockCreateAccountGroups.mockResolvedValue([]);
769833 mockImportAccount.mockClear();
770834 mockDispatch.mockClear();
835+ mockIsUnlocked.mockReset();
836+ mockIsUnlocked.mockReturnValue(true);
837+ mockSubmitPassword.mockClear();
838+ (
839+ Engine.context.AccountTreeController.setAccountGroupName as jest.Mock
840+ ).mockClear();
841+ resetMockAccountState();
771842 });
772843
773844 it('dispatches all onboarding flags', async () => {
@@ -792,6 +863,108 @@ describe('AgenticService.install', () => {
792863 expect(result.error).toBe('boom');
793864 });
794865
866+ it('recovers an existing fixture vault when auth leaves the keyring locked', async () => {
867+ mockIsUnlocked
868+ .mockReturnValueOnce(false)
869+ .mockReturnValueOnce(false)
870+ .mockReturnValue(true);
871+
872+ const result = await bridge().applyWalletFixture({
873+ password: 'test123',
874+ accounts: [],
875+ });
876+
877+ expect(result.ok).toBe(true);
878+ expect(mockSubmitPassword).toHaveBeenCalledWith('test123');
879+ expect(mockDispatch).toHaveBeenCalledWith({ type: 'PASSWORD_SET' });
880+ expect(mockDispatch).toHaveBeenCalledWith({
881+ type: 'SEED_PHRASE_BACKED_UP',
882+ });
883+ expect(mockDispatch).toHaveBeenCalledWith({
884+ type: 'SET_COMPLETED_ONBOARDING',
885+ });
886+ expect(mockDispatch).toHaveBeenCalledWith({ type: 'SET_EXISTING_USER' });
887+ expect(mockDispatch).toHaveBeenCalledWith({ type: 'LOG_IN' });
888+ });
889+
890+ it('creates missing fixture HD accounts with a batched account-group range', async () => {
891+ const mnemonic =
892+ 'test test test test test test test test test test test junk';
893+ const group0Id = `${FIXTURE_WALLET_ID}/0` as const;
894+ Engine.context.AccountsController.state.internalAccounts.accounts = {
895+ a1: mockEvmAccount(
896+ 'a1',
897+ '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
898+ 'dev1',
899+ ),
900+ };
901+ Engine.context.AccountTreeController.state.accountTree = {
902+ wallets: {
903+ [FIXTURE_WALLET_ID]: {
904+ type: AccountWalletType.Entropy,
905+ id: FIXTURE_WALLET_ID,
906+ status: 'ready',
907+ metadata: { name: 'Fixture Wallet', entropy: { id: 'keyring-1' } },
908+ groups: {
909+ [group0Id]: mockEntropyGroup(0, ['a1'], 'dev1'),
910+ },
911+ },
912+ },
913+ };
914+ mockCreateAccountGroups.mockImplementationOnce(async () => {
915+ Engine.context.AccountsController.state.internalAccounts.accounts = {
916+ ...Engine.context.AccountsController.state.internalAccounts.accounts,
917+ a2: mockEvmAccount(
918+ 'a2',
919+ '0x0000000000000000000000000000000000000002',
920+ 'dev2',
921+ ),
922+ a3: mockEvmAccount(
923+ 'a3',
924+ '0x0000000000000000000000000000000000000003',
925+ 'dev3',
926+ ),
927+ };
928+ const wallet = Engine.context.AccountTreeController.state.accountTree
929+ .wallets[FIXTURE_WALLET_ID] as {
930+ groups: Record<string, unknown>;
931+ };
932+ wallet.groups[`${FIXTURE_WALLET_ID}/1`] = mockEntropyGroup(
933+ 1,
934+ ['a2'],
935+ 'dev2',
936+ );
937+ wallet.groups[`${FIXTURE_WALLET_ID}/2`] = mockEntropyGroup(
938+ 2,
939+ ['a3'],
940+ 'dev3',
941+ );
942+ return [];
943+ });
944+
945+ const result = await bridge().setupWallet({
946+ password: 'test123',
947+ accounts: [
948+ {
949+ type: 'mnemonic',
950+ value: mnemonic,
951+ count: 3,
952+ names: ['dev1', 'dev2', 'dev3'],
953+ },
954+ ],
955+ });
956+
957+ expect(result.ok).toBe(true);
958+ expect(mockCreateAccountGroups).toHaveBeenCalledWith({
959+ fromGroupIndex: 1,
960+ toGroupIndex: 2,
961+ entropySource: 'keyring-1',
962+ });
963+ expect(
964+ Engine.context.AccountTreeController.setAccountGroupName,
965+ ).toHaveBeenCalledWith(`${FIXTURE_WALLET_ID}/2`, 'dev3');
966+ });
967+
795968 it('opts out of metametrics when specified', async () => {
796969 const { analytics } = jest.requireMock('../../util/analytics/analytics');
797970 await bridge().setupWallet({
0 commit comments