@@ -10,6 +10,7 @@ import type {
1010 AccountTransactionsUpdatedEventPayload ,
1111 AccountAssetListUpdatedEventPayload ,
1212 MetaMaskOptions ,
13+ CreateAccountOptions ,
1314} from '@metamask/keyring-api' ;
1415import {
1516 EthScope ,
@@ -28,6 +29,7 @@ import {
2829 TrxScope ,
2930 TrxMethod ,
3031 TrxAccountType ,
32+ AccountCreationType ,
3133} from '@metamask/keyring-api' ;
3234import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk' ;
3335import type { JsonRpcRequest } from '@metamask/keyring-utils' ;
@@ -2520,6 +2522,224 @@ describe('SnapKeyring', () => {
25202522 } ) ;
25212523 } ) ;
25222524
2525+ describe ( 'createAccounts' , ( ) => {
2526+ const newAccount1 = {
2527+ ...newEthEoaAccount ,
2528+ id : 'aa11bb22-cc33-4d44-8e55-ff6677889900' ,
2529+ address : '0xaabbccddee00112233445566778899aabbccddee' ,
2530+ } ;
2531+ const newAccount2 = {
2532+ ...newEthEoaAccount ,
2533+ id : 'bb11bb22-cc33-4d44-9e55-ff6677889900' ,
2534+ address : '0xbbccddee00112233445566778899aabbccddeeff' ,
2535+ } ;
2536+ const newAccount3 = {
2537+ ...newEthEoaAccount ,
2538+ id : 'cc11bb22-cc33-4d44-ae55-ff6677889900' ,
2539+ address : '0xccddee00112233445566778899aabbccddee0011' ,
2540+ } ;
2541+
2542+ const entropySource = '01JQCAKR17JARQXZ0NDP760N1K' ;
2543+
2544+ const snapMetadata = {
2545+ manifest : {
2546+ proposedName : 'snap-name' ,
2547+ } ,
2548+ id : snapId ,
2549+ enabled : true ,
2550+ } ;
2551+
2552+ it ( 'creates multiple accounts' , async ( ) => {
2553+ mockCallbacks . addAccount . mockClear ( ) ;
2554+ mockCallbacks . saveState . mockClear ( ) ;
2555+
2556+ mockMessenger . get . mockReturnValue ( snapMetadata ) ;
2557+
2558+ const accountsToCreate = [ newAccount1 , newAccount2 , newAccount3 ] ;
2559+
2560+ mockMessengerHandleRequest ( {
2561+ [ KeyringRpcMethod . CreateAccounts ] : async ( ) => {
2562+ // Unlike createAccount, createAccounts does NOT emit AccountCreated events
2563+ // for each account. It returns all accounts directly.
2564+ return accountsToCreate ;
2565+ } ,
2566+ } ) ;
2567+
2568+ const options : CreateAccountOptions = {
2569+ type : AccountCreationType . Bip44DeriveIndexRange ,
2570+ entropySource,
2571+ range : {
2572+ from : 0 ,
2573+ to : 2 ,
2574+ } ,
2575+ } ;
2576+ const result = await keyring . createAccounts ( snapId , options ) ;
2577+
2578+ expect ( mockMessenger . handleRequest ) . toHaveBeenLastCalledWith (
2579+ mockKeyringRpcRequest ( KeyringRpcMethod . CreateAccounts , options ) ,
2580+ ) ;
2581+
2582+ // Verify all accounts were returned
2583+ expect ( result ) . toStrictEqual ( accountsToCreate ) ;
2584+
2585+ // Verify all accounts were added to the internal state
2586+ for ( const account of accountsToCreate ) {
2587+ expect ( keyring . getAccountByAddress ( account . address ) ) . toMatchObject ( {
2588+ ...account ,
2589+ metadata : expect . objectContaining ( {
2590+ snap : expect . objectContaining ( {
2591+ id : snapId ,
2592+ } ) ,
2593+ } ) ,
2594+ } ) ;
2595+ }
2596+
2597+ // Verify state was saved once after adding all accounts
2598+ expect ( mockCallbacks . saveState ) . toHaveBeenCalled ( ) ;
2599+
2600+ // IMPORTANT: Unlike createAccount, createAccounts does NOT call addAccount callback
2601+ // because accounts are created in batch
2602+ expect ( mockCallbacks . addAccount ) . not . toHaveBeenCalled ( ) ;
2603+ } ) ;
2604+
2605+ it ( 'creates a single account through createAccounts' , async ( ) => {
2606+ mockCallbacks . addAccount . mockClear ( ) ;
2607+ mockCallbacks . saveState . mockClear ( ) ;
2608+
2609+ mockMessenger . get . mockReturnValue ( snapMetadata ) ;
2610+
2611+ const accountToCreate = [ newAccount1 ] ;
2612+
2613+ mockMessengerHandleRequest ( {
2614+ [ KeyringRpcMethod . CreateAccounts ] : async ( ) => accountToCreate ,
2615+ } ) ;
2616+
2617+ const options : CreateAccountOptions = {
2618+ type : AccountCreationType . Bip44DeriveIndex ,
2619+ groupIndex : 0 ,
2620+ entropySource,
2621+ } ;
2622+ const result = await keyring . createAccounts ( snapId , options ) ;
2623+
2624+ expect ( mockMessenger . handleRequest ) . toHaveBeenLastCalledWith (
2625+ mockKeyringRpcRequest ( KeyringRpcMethod . CreateAccounts , options ) ,
2626+ ) ;
2627+
2628+ expect ( result ) . toStrictEqual ( accountToCreate ) ;
2629+ expect ( result ) . toHaveLength ( 1 ) ;
2630+
2631+ // Verify the account was added to the internal state
2632+ expect ( keyring . getAccountByAddress ( newAccount1 . address ) ) . toMatchObject ( {
2633+ ...newAccount1 ,
2634+ metadata : expect . objectContaining ( {
2635+ snap : expect . objectContaining ( {
2636+ id : snapId ,
2637+ } ) ,
2638+ } ) ,
2639+ } ) ;
2640+
2641+ expect ( mockCallbacks . saveState ) . toHaveBeenCalled ( ) ;
2642+ expect ( mockCallbacks . addAccount ) . not . toHaveBeenCalled ( ) ;
2643+ } ) ;
2644+
2645+ it ( 'creates accounts with custom options' , async ( ) => {
2646+ mockCallbacks . addAccount . mockClear ( ) ;
2647+ mockCallbacks . saveState . mockClear ( ) ;
2648+
2649+ const accountsToCreate = [ newAccount1 , newAccount2 ] ;
2650+ const options : CreateAccountOptions = {
2651+ type : AccountCreationType . Custom ,
2652+ } ;
2653+
2654+ mockMessengerHandleRequest ( {
2655+ [ KeyringRpcMethod . CreateAccounts ] : async ( ) => accountsToCreate ,
2656+ } ) ;
2657+
2658+ const result = await keyring . createAccounts ( snapId , options ) ;
2659+
2660+ expect ( mockMessenger . handleRequest ) . toHaveBeenLastCalledWith (
2661+ mockKeyringRpcRequest ( KeyringRpcMethod . CreateAccounts , options ) ,
2662+ ) ;
2663+
2664+ expect ( result ) . toStrictEqual ( accountsToCreate ) ;
2665+ expect ( mockCallbacks . saveState ) . toHaveBeenCalled ( ) ;
2666+ expect ( mockCallbacks . addAccount ) . not . toHaveBeenCalled ( ) ;
2667+ } ) ;
2668+
2669+ it ( 'handles empty response from Snap' , async ( ) => {
2670+ mockCallbacks . addAccount . mockClear ( ) ;
2671+ mockCallbacks . saveState . mockClear ( ) ;
2672+
2673+ mockMessengerHandleRequest ( {
2674+ [ KeyringRpcMethod . CreateAccounts ] : async ( ) => [ ] ,
2675+ } ) ;
2676+
2677+ const options : CreateAccountOptions = {
2678+ type : AccountCreationType . Bip44DeriveIndex ,
2679+ entropySource,
2680+ groupIndex : 0 ,
2681+ } ;
2682+ const result = await keyring . createAccounts ( snapId , options ) ;
2683+
2684+ expect ( result ) . toStrictEqual ( [ ] ) ;
2685+ expect ( mockCallbacks . saveState ) . toHaveBeenCalled ( ) ;
2686+ expect ( mockCallbacks . addAccount ) . not . toHaveBeenCalled ( ) ;
2687+ } ) ;
2688+
2689+ it ( 'handles errors from Snap' , async ( ) => {
2690+ mockCallbacks . addAccount . mockClear ( ) ;
2691+ mockCallbacks . saveState . mockClear ( ) ;
2692+
2693+ const errorMessage = 'Failed to create accounts' ;
2694+
2695+ mockMessengerHandleRequest ( {
2696+ [ KeyringRpcMethod . CreateAccounts ] : async ( ) => {
2697+ throw new Error ( errorMessage ) ;
2698+ } ,
2699+ } ) ;
2700+
2701+ const options : CreateAccountOptions = {
2702+ type : AccountCreationType . Bip44DeriveIndex ,
2703+ entropySource,
2704+ groupIndex : 0 ,
2705+ } ;
2706+ await expect ( keyring . createAccounts ( snapId , options ) ) . rejects . toThrow (
2707+ errorMessage ,
2708+ ) ;
2709+
2710+ // State should not be saved if account creation fails
2711+ expect ( mockCallbacks . saveState ) . not . toHaveBeenCalled ( ) ;
2712+ expect ( mockCallbacks . addAccount ) . not . toHaveBeenCalled ( ) ;
2713+ } ) ;
2714+
2715+ it ( 'adds all accounts to the internal map with correct snapId' , async ( ) => {
2716+ mockCallbacks . addAccount . mockClear ( ) ;
2717+ mockCallbacks . saveState . mockClear ( ) ;
2718+
2719+ mockMessenger . get . mockReturnValue ( snapMetadata ) ;
2720+
2721+ const accountsToCreate = [ newAccount1 , newAccount2 ] ;
2722+
2723+ mockMessengerHandleRequest ( {
2724+ [ KeyringRpcMethod . CreateAccounts ] : async ( ) => accountsToCreate ,
2725+ } ) ;
2726+
2727+ const options : CreateAccountOptions = {
2728+ type : AccountCreationType . Bip44DeriveIndex ,
2729+ entropySource,
2730+ groupIndex : 0 ,
2731+ } ;
2732+ await keyring . createAccounts ( snapId , options ) ;
2733+
2734+ // Verify each account is mapped to the correct snapId
2735+ for ( const account of accountsToCreate ) {
2736+ const createdAccount = keyring . getAccountByAddress ( account . address ) ;
2737+ expect ( createdAccount ) . toBeDefined ( ) ;
2738+ expect ( createdAccount ?. metadata . snap ?. id ) . toBe ( snapId ) ;
2739+ }
2740+ } ) ;
2741+ } ) ;
2742+
25232743 describe ( 'resolveAccountAddress' , ( ) => {
25242744 const scope = toCaipChainId (
25252745 KnownCaipNamespace . Eip155 ,
0 commit comments