From 1620161673f4a1864f9cdf816c421b97b5d19590 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:21:52 +1300 Subject: [PATCH 1/6] Add signEIP7702Authorization and corresponding tests to keyring-eth-hd --- packages/keyring-eth-hd/src/index.js | 7 ++++ packages/keyring-eth-hd/test/index.js | 47 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/packages/keyring-eth-hd/src/index.js b/packages/keyring-eth-hd/src/index.js index 48ed04dbb..3faeb2fd1 100644 --- a/packages/keyring-eth-hd/src/index.js +++ b/packages/keyring-eth-hd/src/index.js @@ -13,6 +13,7 @@ import { personalSign, signTypedData, SignTypedDataVersion, + signEIP7702Authorization, } from '@metamask/eth-sig-util'; import { mnemonicToSeed } from '@metamask/key-tree'; import { generateMnemonic } from '@metamask/scure-bip39'; @@ -218,6 +219,12 @@ class HdKeyring { return signTypedData({ privateKey, data: typedData, version }); } + async signEIP7702Authorization(withAccount, authorization, opts) { + const privateKey = this._getPrivateKeyFor(withAccount, opts); + + return signEIP7702Authorization({ privateKey, authorization }); + } + removeAccount(account) { const address = normalize(account); if ( diff --git a/packages/keyring-eth-hd/test/index.js b/packages/keyring-eth-hd/test/index.js index ad34f079c..fb1e1da5f 100644 --- a/packages/keyring-eth-hd/test/index.js +++ b/packages/keyring-eth-hd/test/index.js @@ -5,6 +5,7 @@ import { toBuffer, ecrecover, pubToAddress, + zeroAddress, } from '@ethereumjs/util'; import * as oldMMForkBIP39 from '@metamask/bip39'; import OldHdKeyring from '@metamask/eth-hd-keyring'; @@ -682,6 +683,52 @@ describe('hd-keyring', () => { }); }); + describe('#signEIP7702Authorization', () => { + const chainId = '0x1'; + const nonce = 1; + const contractAddress = '0x0000000000000000000000000000000000000001'; + + const authorization = [chainId, contractAddress, nonce]; + + it('returns the expected value', async () => { + const keyring = new HdKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + const signature = await keyring.signEIP7702Authorization( + firstAcct, + authorization, + ); + expect(signature).not.toBe(contractAddress); + }); + + it('throw error if empty address is passed', async () => { + const keyring = new HdKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + await expect( + keyring.signEIP7702Authorization('', authorization), + ).rejects.toThrow('Must specify address.'); + }); + + it('throw error if address not associated with the current keyring is passed', async () => { + const keyring = new HdKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + await expect( + keyring.signEIP7702Authorization(notKeyringAddress, authorization), + ).rejects.toThrow('HD Keyring - Unable to find matching address.'); + }); + }); + describe('#removeAccount', function () { let keyring; beforeEach(async () => { From 6bad5bc0d7d83779626702ec422e78830ce106f8 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:10:35 +1300 Subject: [PATCH 2/6] Add signEIP7702Authorization and corresponding tests to keyring-eth-simple. Fix corresponding tests in keyring-eth-hd. --- packages/keyring-eth-hd/test/index.js | 14 ++++-- .../src/simple-keyring.test.ts | 47 +++++++++++++++++++ .../keyring-eth-simple/src/simple-keyring.ts | 12 +++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/keyring-eth-hd/test/index.js b/packages/keyring-eth-hd/test/index.js index fb1e1da5f..45692e7ba 100644 --- a/packages/keyring-eth-hd/test/index.js +++ b/packages/keyring-eth-hd/test/index.js @@ -5,13 +5,13 @@ import { toBuffer, ecrecover, pubToAddress, - zeroAddress, } from '@ethereumjs/util'; import * as oldMMForkBIP39 from '@metamask/bip39'; import OldHdKeyring from '@metamask/eth-hd-keyring'; import { normalize, personalSign, + recoverEIP7702Authorization, recoverPersonalSignature, recoverTypedSignature, signTypedData, @@ -684,9 +684,9 @@ describe('hd-keyring', () => { }); describe('#signEIP7702Authorization', () => { - const chainId = '0x1'; + const chainId = 1; const nonce = 1; - const contractAddress = '0x0000000000000000000000000000000000000001'; + const contractAddress = '0x1234567890abcdef1234567890abcdef12345678'; const authorization = [chainId, contractAddress, nonce]; @@ -701,7 +701,13 @@ describe('hd-keyring', () => { firstAcct, authorization, ); - expect(signature).not.toBe(contractAddress); + + const recovered = recoverEIP7702Authorization({ + signature, + authorization, + }); + + expect(recovered.toLowerCase()).toEqual(firstAcct.toLowerCase()); }); it('throw error if empty address is passed', async () => { diff --git a/packages/keyring-eth-simple/src/simple-keyring.test.ts b/packages/keyring-eth-simple/src/simple-keyring.test.ts index 43bbace05..2421e2a0d 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.test.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.test.ts @@ -3,15 +3,19 @@ import { bufferToHex, ecrecover, isValidAddress, + privateToAddress, pubToAddress, stripHexPrefix, toBuffer, } from '@ethereumjs/util'; import { + EIP7702Authorization, encrypt, getEncryptionPublicKey, + hashEIP7702Authorization, MessageTypes, personalSign, + recoverEIP7702Authorization, recoverPersonalSignature, recoverTypedSignature, signTypedData, @@ -543,6 +547,49 @@ describe('simple-keyring', function () { }); }); + describe('#signEIP7702Authorization', function () { + const address = '0x29c76e6ad8f28bb1004902578fb108c507be341b'; + const privKeyHex = + '0x4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'; + const signerAddress = + '0x' + + privateToAddress(Buffer.from(privKeyHex.slice(2), 'hex')).toString('hex'); + + const chainId = 1; + const nonce = 1; + const contractAddress = '0x1234567890abcdef1234567890abcdef12345678'; + + const authorization: EIP7702Authorization = [ + chainId, + contractAddress, + nonce, + ]; + + it('returns the expected value', async function () { + await keyring.deserialize([privKeyHex]); + const signature = await keyring.signEIP7702Authorization( + address, + authorization, + ); + + expect(typeof signature).toBe('string'); + + const recovered = recoverEIP7702Authorization({ + signature, + authorization, + }); + + expect(recovered.toLowerCase()).toEqual(signerAddress.toLowerCase()); + }); + + it('throws an error if the address is not in the keyring', async function () { + await keyring.deserialize([privKeyHex]); + await expect( + keyring.signEIP7702Authorization(notKeyringAddress, authorization), + ).rejects.toThrow('Simple Keyring - Unable to find matching address.'); + }); + }); + describe('#decryptMessage', function () { const address = '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb'; const privateKey = Buffer.from( diff --git a/packages/keyring-eth-simple/src/simple-keyring.ts b/packages/keyring-eth-simple/src/simple-keyring.ts index dbd100a99..dd3b2c13a 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.ts @@ -12,9 +12,11 @@ import { import { concatSig, decrypt, + EIP7702Authorization, getEncryptionPublicKey, normalize, personalSign, + signEIP7702Authorization, signTypedData, SignTypedDataVersion, } from '@metamask/eth-sig-util'; @@ -96,6 +98,16 @@ export default class SimpleKeyring implements Keyring { return signedTx ?? transaction; } + async signEIP7702Authorization( + address: Hex, + authorization: EIP7702Authorization, + opts: KeyringOpt = {}, + ) { + const privateKey = this.#getPrivateKeyFor(address, opts); + + return signEIP7702Authorization({ privateKey, authorization }); + } + // For eth_sign, we need to sign arbitrary data: async signMessage( address: Hex, From d34295d87cc80737de1f8ad6356136f711f34fc6 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 31 Jan 2025 13:52:11 +1300 Subject: [PATCH 3/6] Fix issues identified by linting --- .../keyring-eth-simple/src/simple-keyring.test.ts | 11 ++++++----- packages/keyring-eth-simple/src/simple-keyring.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/keyring-eth-simple/src/simple-keyring.test.ts b/packages/keyring-eth-simple/src/simple-keyring.test.ts index 2421e2a0d..6c4435f8b 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.test.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.test.ts @@ -12,7 +12,6 @@ import { EIP7702Authorization, encrypt, getEncryptionPublicKey, - hashEIP7702Authorization, MessageTypes, personalSign, recoverEIP7702Authorization, @@ -551,9 +550,9 @@ describe('simple-keyring', function () { const address = '0x29c76e6ad8f28bb1004902578fb108c507be341b'; const privKeyHex = '0x4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'; - const signerAddress = - '0x' + - privateToAddress(Buffer.from(privKeyHex.slice(2), 'hex')).toString('hex'); + const signerAddress = `0x${privateToAddress( + Buffer.from(privKeyHex.slice(2), 'hex'), + ).toString('hex')}`; const chainId = 1; const nonce = 1; @@ -579,7 +578,9 @@ describe('simple-keyring', function () { authorization, }); - expect(recovered.toLowerCase()).toEqual(signerAddress.toLowerCase()); + expect(recovered.toLowerCase()).toStrictEqual( + signerAddress.toLowerCase(), + ); }); it('throws an error if the address is not in the keyring', async function () { diff --git a/packages/keyring-eth-simple/src/simple-keyring.ts b/packages/keyring-eth-simple/src/simple-keyring.ts index dd3b2c13a..a9f503877 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.ts @@ -102,7 +102,7 @@ export default class SimpleKeyring implements Keyring { address: Hex, authorization: EIP7702Authorization, opts: KeyringOpt = {}, - ) { + ): Promise { const privateKey = this.#getPrivateKeyFor(address, opts); return signEIP7702Authorization({ privateKey, authorization }); From 2de499cc26c7745a0b72f8ad407b7e6a7688cb44 Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:03:56 +1300 Subject: [PATCH 4/6] Correct camel-casing of xxxEIP to xxxEip. Remove unnecessary whitespace. Improve names of a few tests. Co-authored-by: Daniel Rocha <68558152+danroc@users.noreply.github.com> --- packages/keyring-eth-hd/src/index.js | 3 +-- packages/keyring-eth-hd/test/index.js | 8 ++++---- packages/keyring-eth-simple/src/simple-keyring.test.ts | 6 +++--- packages/keyring-eth-simple/src/simple-keyring.ts | 3 +-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/keyring-eth-hd/src/index.js b/packages/keyring-eth-hd/src/index.js index 3faeb2fd1..6893dc647 100644 --- a/packages/keyring-eth-hd/src/index.js +++ b/packages/keyring-eth-hd/src/index.js @@ -219,9 +219,8 @@ class HdKeyring { return signTypedData({ privateKey, data: typedData, version }); } - async signEIP7702Authorization(withAccount, authorization, opts) { + async signEip7702Authorization(withAccount, authorization, opts) { const privateKey = this._getPrivateKeyFor(withAccount, opts); - return signEIP7702Authorization({ privateKey, authorization }); } diff --git a/packages/keyring-eth-hd/test/index.js b/packages/keyring-eth-hd/test/index.js index 45692e7ba..f0710cd6a 100644 --- a/packages/keyring-eth-hd/test/index.js +++ b/packages/keyring-eth-hd/test/index.js @@ -683,7 +683,7 @@ describe('hd-keyring', () => { }); }); - describe('#signEIP7702Authorization', () => { + describe('#signEip7702Authorization', () => { const chainId = 1; const nonce = 1; const contractAddress = '0x1234567890abcdef1234567890abcdef12345678'; @@ -697,7 +697,7 @@ describe('hd-keyring', () => { numberOfAccounts: 1, }); - const signature = await keyring.signEIP7702Authorization( + const signature = await keyring.signEip7702Authorization( firstAcct, authorization, ); @@ -710,7 +710,7 @@ describe('hd-keyring', () => { expect(recovered.toLowerCase()).toEqual(firstAcct.toLowerCase()); }); - it('throw error if empty address is passed', async () => { + it('throw an error if an empty address is passed', async () => { const keyring = new HdKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, @@ -722,7 +722,7 @@ describe('hd-keyring', () => { ).rejects.toThrow('Must specify address.'); }); - it('throw error if address not associated with the current keyring is passed', async () => { + it('throw an error if the given address is not associated with the current keyring', async () => { const keyring = new HdKeyring(); await keyring.deserialize({ mnemonic: sampleMnemonic, diff --git a/packages/keyring-eth-simple/src/simple-keyring.test.ts b/packages/keyring-eth-simple/src/simple-keyring.test.ts index 6c4435f8b..2114486f7 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.test.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.test.ts @@ -546,7 +546,7 @@ describe('simple-keyring', function () { }); }); - describe('#signEIP7702Authorization', function () { + describe('#signEip7702Authorization', function () { const address = '0x29c76e6ad8f28bb1004902578fb108c507be341b'; const privKeyHex = '0x4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'; @@ -566,7 +566,7 @@ describe('simple-keyring', function () { it('returns the expected value', async function () { await keyring.deserialize([privKeyHex]); - const signature = await keyring.signEIP7702Authorization( + const signature = await keyring.signEip7702Authorization( address, authorization, ); @@ -586,7 +586,7 @@ describe('simple-keyring', function () { it('throws an error if the address is not in the keyring', async function () { await keyring.deserialize([privKeyHex]); await expect( - keyring.signEIP7702Authorization(notKeyringAddress, authorization), + keyring.signEip7702Authorization(notKeyringAddress, authorization), ).rejects.toThrow('Simple Keyring - Unable to find matching address.'); }); }); diff --git a/packages/keyring-eth-simple/src/simple-keyring.ts b/packages/keyring-eth-simple/src/simple-keyring.ts index a9f503877..7aea5dba4 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.ts @@ -98,13 +98,12 @@ export default class SimpleKeyring implements Keyring { return signedTx ?? transaction; } - async signEIP7702Authorization( + async signEip7702Authorization( address: Hex, authorization: EIP7702Authorization, opts: KeyringOpt = {}, ): Promise { const privateKey = this.#getPrivateKeyFor(address, opts); - return signEIP7702Authorization({ privateKey, authorization }); } From 9cb340fc4a9d1cffc8634da1737465cf05248a8a Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:50:42 +1300 Subject: [PATCH 5/6] Add explicit return type to signAuthorization, and fix return type in comment. --- packages/keyring-eth-hd/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/keyring-eth-hd/src/index.ts b/packages/keyring-eth-hd/src/index.ts index c5bbd72ff..70639c4a5 100644 --- a/packages/keyring-eth-hd/src/index.ts +++ b/packages/keyring-eth-hd/src/index.ts @@ -376,13 +376,13 @@ class HdKeyring { * @param withAccount - The address of the account. * @param authorization - The EIP-7702 authorization to sign. * @param opts - The options for selecting the account. - * @returns The signed authorization. + * @returns The signature of the authorization. */ async signEip7702Authorization( withAccount: Hex, authorization: EIP7702Authorization, opts?: HDKeyringAccountSelectionOptions, - ) { + ): Promise { const privateKey = this.#getPrivateKeyFor(withAccount, opts); return signEIP7702Authorization({ privateKey: Buffer.from(privateKey), From 390fd767873a8bd461de24855029578a895cbf3d Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:53:39 +1300 Subject: [PATCH 6/6] Fix linting issues: - prefer .toStrictEqual in signEip7702Authorization test - use type imports where possible --- packages/keyring-eth-hd/src/index.ts | 2 +- packages/keyring-eth-hd/test/index.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/keyring-eth-hd/src/index.ts b/packages/keyring-eth-hd/src/index.ts index 70639c4a5..f340593d9 100644 --- a/packages/keyring-eth-hd/src/index.ts +++ b/packages/keyring-eth-hd/src/index.ts @@ -9,7 +9,7 @@ import { import { concatSig, decrypt, - EIP7702Authorization, + type EIP7702Authorization, type EthEncryptedData, getEncryptionPublicKey, type MessageTypes, diff --git a/packages/keyring-eth-hd/test/index.test.ts b/packages/keyring-eth-hd/test/index.test.ts index 10e2cf620..fa1726c2f 100644 --- a/packages/keyring-eth-hd/test/index.test.ts +++ b/packages/keyring-eth-hd/test/index.test.ts @@ -19,7 +19,7 @@ import { type EthEncryptedData, type TypedMessage, type MessageTypes, - EIP7702Authorization, + type EIP7702Authorization, } from '@metamask/eth-sig-util'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import { assert, type Hex } from '@metamask/utils'; @@ -726,7 +726,7 @@ describe('hd-keyring', () => { authorization, }); - expect(recovered.toLowerCase()).toEqual(firstAcct.toLowerCase()); + expect(recovered.toLowerCase()).toStrictEqual(firstAcct.toLowerCase()); }); it('throw an error if an empty address is passed', async () => {