@@ -947,6 +947,82 @@ describe('LedgerKeyring', function () {
947947 keyring . signPersonalMessage ( fakeAccounts [ 0 ] , 'some message' ) ,
948948 ) . rejects . toThrow ( 'Some other transport error' ) ;
949949 } ) ;
950+
951+ it ( 'normalizes v=0 to v=27 for proper signature recovery' , async function ( ) {
952+ await basicSetupToUnlockOneAccount ( ) ;
953+ jest
954+ . spyOn ( keyring . bridge , 'deviceSignMessage' )
955+ . mockResolvedValue ( { v : 0 , r : 'aabbccdd' , s : '11223344' } ) ;
956+
957+ jest
958+ . spyOn ( sigUtil , 'recoverPersonalSignature' )
959+ . mockReturnValue ( fakeAccounts [ 0 ] ) ;
960+
961+ const result = await keyring . signPersonalMessage (
962+ fakeAccounts [ 0 ] ,
963+ 'some message' ,
964+ ) ;
965+
966+ // v=0 should be normalized to v=27 (0x1b)
967+ expect ( result ) . toBe ( '0xaabbccdd112233441b' ) ;
968+ } ) ;
969+
970+ it ( 'normalizes v=1 to v=28 for proper signature recovery' , async function ( ) {
971+ await basicSetupToUnlockOneAccount ( ) ;
972+ jest
973+ . spyOn ( keyring . bridge , 'deviceSignMessage' )
974+ . mockResolvedValue ( { v : 1 , r : 'aabbccdd' , s : '11223344' } ) ;
975+
976+ jest
977+ . spyOn ( sigUtil , 'recoverPersonalSignature' )
978+ . mockReturnValue ( fakeAccounts [ 0 ] ) ;
979+
980+ const result = await keyring . signPersonalMessage (
981+ fakeAccounts [ 0 ] ,
982+ 'some message' ,
983+ ) ;
984+
985+ // v=1 should be normalized to v=28 (0x1c)
986+ expect ( result ) . toBe ( '0xaabbccdd112233441c' ) ;
987+ } ) ;
988+
989+ it ( 'preserves v=27 unchanged' , async function ( ) {
990+ await basicSetupToUnlockOneAccount ( ) ;
991+ jest
992+ . spyOn ( keyring . bridge , 'deviceSignMessage' )
993+ . mockResolvedValue ( { v : 27 , r : 'aabbccdd' , s : '11223344' } ) ;
994+
995+ jest
996+ . spyOn ( sigUtil , 'recoverPersonalSignature' )
997+ . mockReturnValue ( fakeAccounts [ 0 ] ) ;
998+
999+ const result = await keyring . signPersonalMessage (
1000+ fakeAccounts [ 0 ] ,
1001+ 'some message' ,
1002+ ) ;
1003+
1004+ // v=27 should remain as 0x1b
1005+ expect ( result ) . toBe ( '0xaabbccdd112233441b' ) ;
1006+ } ) ;
1007+
1008+ it ( 'preserves v=28 unchanged' , async function ( ) {
1009+ await basicSetupToUnlockOneAccount ( ) ;
1010+ jest
1011+ . spyOn ( keyring . bridge , 'deviceSignMessage' )
1012+ . mockResolvedValue ( { v : 28 , r : 'aabbccdd' , s : '11223344' } ) ;
1013+
1014+ jest
1015+ . spyOn ( sigUtil , 'recoverPersonalSignature' )
1016+ . mockReturnValue ( fakeAccounts [ 0 ] ) ;
1017+
1018+ const result = await keyring . signPersonalMessage (
1019+ fakeAccounts [ 0 ] ,
1020+ 'some message' ,
1021+ ) ;
1022+
1023+ // v=28 should remain as 0x1c
1024+ expect ( result ) . toBe ( '0xaabbccdd112233441c' ) ;
1025+ } ) ;
9501026 } ) ;
9511027
9521028 describe ( 'signMessage' , function ( ) {
@@ -1279,7 +1355,9 @@ describe('LedgerKeyring', function () {
12791355 ) . rejects . toThrow ( 'Ledger: Unknown error while signing message' ) ;
12801356 } ) ;
12811357
1282- it ( 'returns signature when recoveryId length < 2' , async function ( ) {
1358+ it ( 'normalizes v=0 to v=27 for proper signature recovery' , async function ( ) {
1359+ // Ledger may return v as 0 or 1 (modern format), but signature
1360+ // recovery expects 27 or 28 (legacy format)
12831361 jest . spyOn ( keyring . bridge , 'deviceSignTypedData' ) . mockResolvedValue ( {
12841362 v : 0 ,
12851363 r : '72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b9' ,
@@ -1292,8 +1370,45 @@ describe('LedgerKeyring', function () {
12921370 version : sigUtil . SignTypedDataVersion . V4 ,
12931371 } ,
12941372 ) ;
1373+ // v=0 should be normalized to v=27 (0x1b)
1374+ expect ( result ) . toBe (
1375+ '0x72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b946759735981cd0c3efb02d36df28bb2feedfec3d90e408efc93f45b894946e321b' ,
1376+ ) ;
1377+ } ) ;
1378+
1379+ it ( 'normalizes v=1 to v=28 for proper signature recovery' , async function ( ) {
1380+ jest . spyOn ( keyring . bridge , 'deviceSignTypedData' ) . mockResolvedValue ( {
1381+ v : 1 ,
1382+ r : '72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b9' ,
1383+ s : '46759735981cd0c3efb02d36df28bb2feedfec3d90e408efc93f45b894946e32' ,
1384+ } ) ;
1385+
1386+ // v=1 should be normalized to v=28 (0x1c), but this will fail
1387+ // address recovery since the correct v for this signature is 27
1388+ await expect (
1389+ keyring . signTypedData ( fakeAccounts [ 15 ] , fixtureData , {
1390+ version : sigUtil . SignTypedDataVersion . V4 ,
1391+ } ) ,
1392+ ) . rejects . toThrow (
1393+ 'Ledger: The signature doesnt match the right address' ,
1394+ ) ;
1395+ } ) ;
1396+
1397+ it ( 'preserves v=27 unchanged' , async function ( ) {
1398+ jest . spyOn ( keyring . bridge , 'deviceSignTypedData' ) . mockResolvedValue ( {
1399+ v : 27 ,
1400+ r : '72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b9' ,
1401+ s : '46759735981cd0c3efb02d36df28bb2feedfec3d90e408efc93f45b894946e32' ,
1402+ } ) ;
1403+ const result = await keyring . signTypedData (
1404+ fakeAccounts [ 15 ] ,
1405+ fixtureData ,
1406+ {
1407+ version : sigUtil . SignTypedDataVersion . V4 ,
1408+ } ,
1409+ ) ;
12951410 expect ( result ) . toBe (
1296- '0x72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b946759735981cd0c3efb02d36df28bb2feedfec3d90e408efc93f45b894946e3200 ' ,
1411+ '0x72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b946759735981cd0c3efb02d36df28bb2feedfec3d90e408efc93f45b894946e321b ' ,
12971412 ) ;
12981413 } ) ;
12991414
@@ -1421,6 +1536,22 @@ describe('LedgerKeyring', function () {
14211536 expect ( result ) . toStrictEqual ( mockResponse ) ;
14221537 expect ( result . arbitraryDataEnabled ) . toBe ( 0 ) ;
14231538 } ) ;
1539+
1540+ it ( 'handles TransportStatusError when getting app configuration' , async function ( ) {
1541+ const transportError = {
1542+ statusCode : 27013 ,
1543+ message : 'Ledger device: (denied by the user?) (0x6985)' ,
1544+ name : 'TransportStatusError' ,
1545+ } ;
1546+ Object . setPrototypeOf ( transportError , TransportStatusError . prototype ) ;
1547+ jest
1548+ . spyOn ( keyring . bridge , 'getAppConfiguration' )
1549+ . mockRejectedValue ( transportError ) ;
1550+
1551+ await expect ( keyring . getAppConfiguration ( ) ) . rejects . toThrow (
1552+ 'User rejected action on device' ,
1553+ ) ;
1554+ } ) ;
14241555 } ) ;
14251556
14261557 describe ( 'destroy' , function ( ) {
0 commit comments