diff --git a/packages/keyring-eth-hd/src/index.ts b/packages/keyring-eth-hd/src/index.ts index a791dfe60..f340593d9 100644 --- a/packages/keyring-eth-hd/src/index.ts +++ b/packages/keyring-eth-hd/src/index.ts @@ -9,11 +9,13 @@ import { import { concatSig, decrypt, + type EIP7702Authorization, type EthEncryptedData, getEncryptionPublicKey, type MessageTypes, normalize, personalSign, + signEIP7702Authorization, signTypedData, SignTypedDataVersion, type TypedDataV1, @@ -367,6 +369,27 @@ class HdKeyring { }); } + /** + * Sign an EIP-7702 authorization using the private key of the specified account. + * This method is compatible with the EIP-7702 standard for enabling smart contract code for EOAs. + * + * @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 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), + authorization, + }); + } + /** * Remove an account from the keyring. * diff --git a/packages/keyring-eth-hd/test/index.test.ts b/packages/keyring-eth-hd/test/index.test.ts index 8e85e727e..fa1726c2f 100644 --- a/packages/keyring-eth-hd/test/index.test.ts +++ b/packages/keyring-eth-hd/test/index.test.ts @@ -10,6 +10,7 @@ import * as oldMMForkBIP39 from '@metamask/bip39'; import { normalize, personalSign, + recoverEIP7702Authorization, recoverPersonalSignature, recoverTypedSignature, signTypedData, @@ -18,6 +19,7 @@ import { type EthEncryptedData, type TypedMessage, type MessageTypes, + type EIP7702Authorization, } from '@metamask/eth-sig-util'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import { assert, type Hex } from '@metamask/utils'; @@ -696,6 +698,62 @@ describe('hd-keyring', () => { }); }); + describe('#signEip7702Authorization', () => { + const chainId = 1; + const nonce = 1; + const contractAddress = '0x1234567890abcdef1234567890abcdef12345678'; + + const authorization: EIP7702Authorization = [ + 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, + ); + + const recovered = recoverEIP7702Authorization({ + signature, + authorization, + }); + + expect(recovered.toLowerCase()).toStrictEqual(firstAcct.toLowerCase()); + }); + + it('throw an error if an empty address is passed', async () => { + const keyring = new HdKeyring(); + await keyring.deserialize({ + mnemonic: sampleMnemonic, + numberOfAccounts: 1, + }); + + await expect( + keyring.signEip7702Authorization('' as Hex, authorization), + ).rejects.toThrow('Must specify address.'); + }); + + 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, + numberOfAccounts: 1, + }); + + await expect( + keyring.signEip7702Authorization(notKeyringAddress, authorization), + ).rejects.toThrow('HD Keyring - Unable to find matching address.'); + }); + }); + describe('#removeAccount', function () { let keyring: HdKeyring; beforeEach(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..2114486f7 100644 --- a/packages/keyring-eth-simple/src/simple-keyring.test.ts +++ b/packages/keyring-eth-simple/src/simple-keyring.test.ts @@ -3,15 +3,18 @@ import { bufferToHex, ecrecover, isValidAddress, + privateToAddress, pubToAddress, stripHexPrefix, toBuffer, } from '@ethereumjs/util'; import { + EIP7702Authorization, encrypt, getEncryptionPublicKey, MessageTypes, personalSign, + recoverEIP7702Authorization, recoverPersonalSignature, recoverTypedSignature, signTypedData, @@ -543,6 +546,51 @@ 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()).toStrictEqual( + 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..7aea5dba4 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,15 @@ export default class SimpleKeyring implements Keyring { return signedTx ?? transaction; } + async signEip7702Authorization( + address: Hex, + authorization: EIP7702Authorization, + opts: KeyringOpt = {}, + ): Promise { + const privateKey = this.#getPrivateKeyFor(address, opts); + return signEIP7702Authorization({ privateKey, authorization }); + } + // For eth_sign, we need to sign arbitrary data: async signMessage( address: Hex,