11import type { AccountGroupId } from '@metamask/account-api' ;
22import { SNAP_KEYRING_TYPE } from '@metamask/eth-snap-keyring' ;
33import type { SnapKeyring , SnapMessage } from '@metamask/eth-snap-keyring' ;
4+ import type { SnapKeyring as SnapKeyringV2 } from '@metamask/eth-snap-keyring/v2' ;
5+ import { KeyringEvent } from '@metamask/keyring-api' ;
46import { KeyringType } from '@metamask/keyring-api/v2' ;
57import {
8+ KeyringControllerError ,
9+ KeyringControllerErrorMessage ,
610 KeyringControllerState ,
711 KeyringTypes ,
812} from '@metamask/keyring-controller' ;
913import type {
1014 KeyringEntry ,
15+ KeyringMetadata ,
16+ KeyringSelectorV2 ,
1117 RestrictedController ,
1218} from '@metamask/keyring-controller' ;
19+ import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk' ;
1320import { Messenger , MOCK_ANY_NAMESPACE } from '@metamask/messenger' ;
1421import type {
1522 MockAnyNamespace ,
@@ -64,6 +71,7 @@ type Mocks = {
6471 KeyringController : {
6572 getState : jest . MockedFunction < ( ) => { keyrings : { type : string } [ ] } > ;
6673 withController : jest . Mock ;
74+ withKeyringV2 : jest . Mock ;
6775 } ;
6876 // eslint-disable-next-line @typescript-eslint/naming-convention
6977 AccountTreeController : {
@@ -105,6 +113,7 @@ function getMessenger(
105113 'SnapController:getRunnableSnaps' ,
106114 'KeyringController:getState' ,
107115 'KeyringController:withController' ,
116+ 'KeyringController:withKeyringV2' ,
108117 'AccountTreeController:getAccountGroupObject' ,
109118 'AccountTreeController:getSelectedAccountGroup' ,
110119 ] ,
@@ -317,6 +326,57 @@ function mockWithController(
317326 return { addNewKeyring } ;
318327}
319328
329+ /**
330+ * Configures `mocks.KeyringController.withKeyringV2` so that the operation
331+ * receives a Snap keyring v2 matching the given selector, or throws
332+ * `KeyringNotFound` when none matches.
333+ *
334+ * @param mocks - The mocks object from {@link setup}.
335+ * @param keyrings - The available v2 Snap keyrings, keyed by snap ID.
336+ */
337+ function mockWithKeyringV2 (
338+ mocks : Mocks ,
339+ keyrings : Record < string , Pick < SnapKeyringV2 , 'handleKeyringSnapMessage' > > ,
340+ ) : void {
341+ mocks . KeyringController . withKeyringV2 . mockImplementation (
342+ async (
343+ selector : KeyringSelectorV2 ,
344+ operation : ( args : {
345+ keyring : SnapKeyringV2 ;
346+ metadata : KeyringMetadata ;
347+ } ) => Promise < unknown > ,
348+ ) => {
349+ const entry = Object . entries ( keyrings ) . find ( ( [ snapId , kr ] ) =>
350+ // The selector's filter expects a v2 keyring object; we synthesise
351+ // a minimal shape (`type` + `snapId`) so the production filter
352+ // function can identify it.
353+ selector . filter ?.(
354+ {
355+ type : KeyringType . Snap ,
356+ snapId,
357+ ...kr ,
358+ } as unknown ,
359+ { id : `id-${ snapId } ` , name : 'snap' } as KeyringMetadata ,
360+ ) ,
361+ ) ;
362+ if ( ! entry ) {
363+ throw new KeyringControllerError (
364+ KeyringControllerErrorMessage . KeyringNotFound ,
365+ ) ;
366+ }
367+ const [ snapId , kr ] = entry ;
368+ return operation ( {
369+ keyring : {
370+ type : KeyringType . Snap ,
371+ snapId,
372+ ...kr ,
373+ } as unknown as SnapKeyringV2 ,
374+ metadata : { id : `id-${ snapId } ` , name : 'snap' } as KeyringMetadata ,
375+ } ) ;
376+ } ,
377+ ) ;
378+ }
379+
320380/**
321381 * Configures `mocks.KeyringController.withController` to expose a single
322382 * legacy Snap keyring with the provided mocked methods.
@@ -400,6 +460,7 @@ function setup({
400460 KeyringController : {
401461 getState : jest . fn ( ) . mockReturnValue ( { keyrings } ) ,
402462 withController : jest . fn ( ) ,
463+ withKeyringV2 : jest . fn ( ) ,
403464 } ,
404465 AccountTreeController : {
405466 getAccountGroupObject : jest . fn ( ) . mockReturnValue ( undefined ) ,
@@ -423,6 +484,10 @@ function setup({
423484 'KeyringController:withController' ,
424485 mocks . KeyringController . withController ,
425486 ) ;
487+ rootMessenger . registerActionHandler (
488+ 'KeyringController:withKeyringV2' ,
489+ mocks . KeyringController . withKeyringV2 ,
490+ ) ;
426491 rootMessenger . registerActionHandler (
427492 'AccountTreeController:getAccountGroupObject' ,
428493 mocks . AccountTreeController . getAccountGroupObject ,
@@ -833,44 +898,122 @@ describe('SnapAccountService', () => {
833898
834899 describe ( 'handleKeyringSnapMessage' , ( ) => {
835900 const MOCK_MESSAGE = {
836- method : 'keyring_listAccounts' ,
901+ method : KeyringEvent . AccountUpdated ,
902+ params : { } ,
903+ } as unknown as SnapMessage ;
904+ const MOCK_ACCOUNT_CREATED_MESSAGE = {
905+ method : KeyringEvent . AccountCreated ,
837906 params : { } ,
838907 } as unknown as SnapMessage ;
908+ const MOCK_GROUP_ID = 'keyring:01JABC/group-1' as AccountGroupId ;
909+ const MOCK_ACCOUNTS = [
910+ '00000000-0000-0000-0000-000000000001' ,
911+ '00000000-0000-0000-0000-000000000002' ,
912+ ] ;
839913
840- it ( 'forwards the call to the legacy Snap keyring and returns its result' , async ( ) => {
914+ it ( 'forwards the message to the matching v2 Snap keyring and returns its result' , async ( ) => {
841915 const { service, mocks } = setup ( ) ;
842916 const handleKeyringSnapMessage = jest
843917 . fn ( )
844918 . mockResolvedValue ( { ok : true } ) ;
845- mockLegacySnapKeyring ( mocks , { handleKeyringSnapMessage } ) ;
919+ mockWithKeyringV2 ( mocks , {
920+ [ MOCK_SNAP_ID ] : { handleKeyringSnapMessage } ,
921+ } ) ;
846922
847923 const result = await service . handleKeyringSnapMessage (
848924 MOCK_SNAP_ID ,
849925 MOCK_MESSAGE ,
850926 ) ;
851927
852- expect ( handleKeyringSnapMessage ) . toHaveBeenCalledWith (
853- MOCK_SNAP_ID ,
854- MOCK_MESSAGE ,
855- ) ;
928+ expect ( handleKeyringSnapMessage ) . toHaveBeenCalledWith ( MOCK_MESSAGE ) ;
856929 expect ( result ) . toStrictEqual ( { ok : true } ) ;
857930 } ) ;
858931
859- it ( 'propagates errors thrown by the Snap keyring' , async ( ) => {
932+ it ( 'short-circuits the GetSelectedAccounts method by returning the selected account group accounts' , async ( ) => {
933+ const { service, mocks } = setup ( ) ;
934+ mocks . AccountTreeController . getSelectedAccountGroup . mockReturnValue (
935+ MOCK_GROUP_ID ,
936+ ) ;
937+ mocks . AccountTreeController . getAccountGroupObject . mockReturnValue (
938+ buildGroup ( MOCK_GROUP_ID , MOCK_ACCOUNTS ) ,
939+ ) ;
940+
941+ const result = await service . handleKeyringSnapMessage ( MOCK_SNAP_ID , {
942+ method : SnapManageAccountsMethod . GetSelectedAccounts ,
943+ params : { } ,
944+ } as unknown as SnapMessage ) ;
945+
946+ expect ( result ) . toStrictEqual ( MOCK_ACCOUNTS ) ;
947+ expect ( mocks . KeyringController . withKeyringV2 ) . not . toHaveBeenCalled ( ) ;
948+ } ) ;
949+
950+ it ( 'returns an empty array for GetSelectedAccounts when no account group is selected' , async ( ) => {
951+ const { service } = setup ( ) ;
952+
953+ const result = await service . handleKeyringSnapMessage ( MOCK_SNAP_ID , {
954+ method : SnapManageAccountsMethod . GetSelectedAccounts ,
955+ params : { } ,
956+ } as unknown as SnapMessage ) ;
957+
958+ expect ( result ) . toStrictEqual ( [ ] ) ;
959+ } ) ;
960+
961+ it ( 'throws a dedicated error when no v2 Snap keyring exists for the given Snap' , async ( ) => {
962+ const { service, mocks } = setup ( ) ;
963+ mockWithKeyringV2 ( mocks , { } ) ;
964+
965+ await expect (
966+ service . handleKeyringSnapMessage ( MOCK_SNAP_ID , MOCK_MESSAGE ) ,
967+ ) . rejects . toThrow (
968+ `Cannot delegate keyring Snap message, keyring does not exist yet for Snap "${ MOCK_SNAP_ID } ".` ,
969+ ) ;
970+ } ) ;
971+
972+ it ( 'propagates non-KeyringNotFound errors thrown by the Snap keyring' , async ( ) => {
860973 const { service, mocks } = setup ( ) ;
861974 const error = new Error ( 'snap boom' ) ;
862975 const handleKeyringSnapMessage = jest . fn ( ) . mockRejectedValue ( error ) ;
863- mockLegacySnapKeyring ( mocks , { handleKeyringSnapMessage } ) ;
976+ mockWithKeyringV2 ( mocks , {
977+ [ MOCK_SNAP_ID ] : { handleKeyringSnapMessage } ,
978+ } ) ;
864979
865980 await expect (
866981 service . handleKeyringSnapMessage ( MOCK_SNAP_ID , MOCK_MESSAGE ) ,
867982 ) . rejects . toThrow ( error ) ;
868983 } ) ;
869984
985+ it ( 'ensures the v2 keyring exists before forwarding an AccountCreated event' , async ( ) => {
986+ const { service, mocks } = setup ( ) ;
987+ // `#ensureKeyringIsReady` uses `withController` — start with no keyring
988+ // so it must create one.
989+ const { addNewKeyring } = mockWithController ( mocks , [ ] ) ;
990+ const handleKeyringSnapMessage = jest . fn ( ) . mockResolvedValue ( null ) ;
991+ // `withController` mock takes precedence over `withKeyringV2`; configure
992+ // `withKeyringV2` separately for the forwarding step.
993+ mockWithKeyringV2 ( mocks , {
994+ [ MOCK_SNAP_ID ] : { handleKeyringSnapMessage } ,
995+ } ) ;
996+
997+ await service . handleKeyringSnapMessage (
998+ MOCK_SNAP_ID ,
999+ MOCK_ACCOUNT_CREATED_MESSAGE ,
1000+ ) ;
1001+
1002+ expect ( addNewKeyring ) . toHaveBeenCalledWith ( KeyringType . Snap , {
1003+ snapId : MOCK_SNAP_ID ,
1004+ accounts : { } ,
1005+ } ) ;
1006+ expect ( handleKeyringSnapMessage ) . toHaveBeenCalledWith (
1007+ MOCK_ACCOUNT_CREATED_MESSAGE ,
1008+ ) ;
1009+ } ) ;
1010+
8701011 it ( 'is exposed as a messenger action' , async ( ) => {
8711012 const { service, mocks, messenger } = setup ( ) ;
8721013 const handleKeyringSnapMessage = jest . fn ( ) . mockResolvedValue ( 'pong' ) ;
873- mockLegacySnapKeyring ( mocks , { handleKeyringSnapMessage } ) ;
1014+ mockWithKeyringV2 ( mocks , {
1015+ [ MOCK_SNAP_ID ] : { handleKeyringSnapMessage } ,
1016+ } ) ;
8741017
8751018 // Reference `service` so it isn't flagged as unused; constructing it
8761019 // registers the messenger action under test.
@@ -882,10 +1025,7 @@ describe('SnapAccountService', () => {
8821025 MOCK_MESSAGE ,
8831026 ) ;
8841027
885- expect ( handleKeyringSnapMessage ) . toHaveBeenCalledWith (
886- MOCK_SNAP_ID ,
887- MOCK_MESSAGE ,
888- ) ;
1028+ expect ( handleKeyringSnapMessage ) . toHaveBeenCalledWith ( MOCK_MESSAGE ) ;
8891029 expect ( result ) . toBe ( 'pong' ) ;
8901030 } ) ;
8911031 } ) ;
0 commit comments