From 5672955923dc28b8edd58a6ae58a5cd9365bbc8b Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:29:23 +1300 Subject: [PATCH 1/5] Add methods for signing, hashing, and recovering authorizations, as per 7702 --- src/sign-authorization.test.ts | 244 +++++++++++++++++++++++++++++++++ src/sign-authorization.ts | 127 +++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 src/sign-authorization.test.ts create mode 100644 src/sign-authorization.ts diff --git a/src/sign-authorization.test.ts b/src/sign-authorization.test.ts new file mode 100644 index 00000000..66c2ebb4 --- /dev/null +++ b/src/sign-authorization.test.ts @@ -0,0 +1,244 @@ +import { bufferToHex, privateToAddress } from '@ethereumjs/util'; +import { + signAuthorization, + recoverAuthorization, + Authorization, + hashAuthorization, +} from './sign-authorization'; + +const testPrivateKey = Buffer.from( + '4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0', + 'hex', +); + +const testAddress = bufferToHex(privateToAddress(testPrivateKey)); + +const testAuthorization: Authorization = [ + 8545, + '0x1234567890123456789012345678901234567890', + 1, +]; + +const expectedAuthorizationHash = Buffer.from( + 'b847dee5b33802280f3279d57574e1eb6bf5d628d7f63049e3cb20bad211056c', + 'hex', +); + +const expectedSignature = + '0xebea1ac12f17a56a514dfecbcbc8bbee7b089fa3fcee31680d1e2c1588f623df7973cab74e12536678995377da38c96c65c52897750b73462c6760ef2737dba41b'; + +describe('signAuthorization', () => { + describe('signAuthorization()', () => { + it('should produce the correct signature', () => { + const signature = signAuthorization({ + privateKey: testPrivateKey, + authorization: testAuthorization, + }); + + expect(signature).toBe(expectedSignature); + }); + + it('should throw if private key is null', () => { + expect(() => + signAuthorization({ + privateKey: null as any, + authorization: testAuthorization, + }), + ).toThrow('Missing privateKey parameter'); + }); + + it('should throw if private key is undefined', () => { + expect(() => + signAuthorization({ + privateKey: undefined as any, + authorization: testAuthorization, + }), + ).toThrow('Missing privateKey parameter'); + }); + + it('should throw if authorization is null', () => { + expect(() => + signAuthorization({ + privateKey: testPrivateKey, + authorization: null as any, + }), + ).toThrow('Missing authorization parameter'); + }); + + it('should throw if authorization is undefined', () => { + expect(() => + signAuthorization({ + privateKey: testPrivateKey, + authorization: undefined as any, + }), + ).toThrow('Missing authorization parameter'); + }); + + it('should throw if chainId is null', () => { + expect(() => + signAuthorization({ + privateKey: testPrivateKey, + authorization: [ + null as unknown as number, + testAuthorization[1], + testAuthorization[2], + ], + }), + ).toThrow('Missing chainId parameter'); + }); + + it('should throw if contractAddress is null', () => { + expect(() => + signAuthorization({ + privateKey: testPrivateKey, + authorization: [ + testAuthorization[0], + null as unknown as string, + testAuthorization[2], + ], + }), + ).toThrow('Missing contractAddress parameter'); + }); + + it('should throw if nonce is null', () => { + expect(() => + signAuthorization({ + privateKey: testPrivateKey, + authorization: [ + testAuthorization[0], + testAuthorization[1], + null as unknown as number, + ], + }), + ).toThrow('Missing nonce parameter'); + }); + }); + + describe('hashAuthorization()', () => { + it('should produce the correct hash', () => { + const hash = hashAuthorization(testAuthorization); + + expect(hash).toEqual(expectedAuthorizationHash); + }); + + it('should throw if authorization is null', () => { + expect(() => hashAuthorization(null as unknown as Authorization)).toThrow( + 'Missing authorization parameter', + ); + }); + + it('should throw if authorization is undefined', () => { + expect(() => + hashAuthorization(undefined as unknown as Authorization), + ).toThrow('Missing authorization parameter'); + }); + + it('should throw if chainId is null', () => { + expect(() => + hashAuthorization([ + null as unknown as number, + testAuthorization[1], + testAuthorization[2], + ]), + ).toThrow('Missing chainId parameter'); + }); + + it('should throw if contractAddress is null', () => { + expect(() => + hashAuthorization([ + testAuthorization[0], + null as unknown as string, + testAuthorization[2], + ]), + ).toThrow('Missing contractAddress parameter'); + }); + + it('should throw if nonce is null', () => { + expect(() => + hashAuthorization([ + testAuthorization[0], + testAuthorization[1], + null as unknown as number, + ]), + ).toThrow('Missing nonce parameter'); + }); + }); + + describe('recoverAuthorization()', () => { + it('should recover the address from a signature', () => { + const recoveredAddress = recoverAuthorization({ + authorization: testAuthorization, + signature: expectedSignature, + }); + + expect(recoveredAddress).toBe(testAddress); + }); + + it('should throw if signature is null', () => { + expect(() => + recoverAuthorization({ + signature: null as unknown as string, + authorization: testAuthorization, + }), + ).toThrow('Missing signature parameter'); + }); + + it('should throw if signature is undefined', () => { + expect(() => + recoverAuthorization({ + signature: undefined as unknown as string, + authorization: testAuthorization, + }), + ).toThrow('Missing signature parameter'); + }); + + it('should throw if authorization is null', () => { + expect(() => + recoverAuthorization({ + signature: expectedSignature, + authorization: null as unknown as Authorization, + }), + ).toThrow('Missing authorization parameter'); + }); + + it('should throw if authorization is undefined', () => { + expect(() => + recoverAuthorization({ + signature: expectedSignature, + authorization: undefined as unknown as Authorization, + }), + ).toThrow('Missing authorization parameter'); + }); + }); + + describe('sign-and-recover', () => { + const testCases = { + 'zero chainId': [0, '0x1234567890123456789012345678901234567890', 1], + 'high chainId': [98765, '0x1234567890123456789012345678901234567890', 1], + 'zero nonce': [8545, '0x1234567890123456789012345678901234567890', 0], + 'high nonce': [8545, '0x1234567890123456789012345678901234567890', 98765], + 'zero contractAddress': [ + 1, + '0x0000000000000000000000000000000000000000', + 1, + ], + 'all zero values': [0, '0x0000000000000000000000000000000000000000', 0], + } as { [key: string]: Authorization }; + + for (const [label, authorization] of Object.entries(testCases)) { + it(`should sign and recover ${label}`, () => { + const signature = signAuthorization({ + privateKey: testPrivateKey, + authorization, + }); + + const recoveredAddress = recoverAuthorization({ + authorization, + signature, + }); + + expect(recoveredAddress).toBe(testAddress); + }); + } + }); +}); diff --git a/src/sign-authorization.ts b/src/sign-authorization.ts new file mode 100644 index 00000000..2c0376b8 --- /dev/null +++ b/src/sign-authorization.ts @@ -0,0 +1,127 @@ +import { ecsign, publicToAddress, toBuffer } from '@ethereumjs/util'; +import { bytesToHex } from '@metamask/utils'; +import { keccak256 } from 'ethereum-cryptography/keccak'; +import { concatSig, isNullish, recoverPublicKey } from './utils'; +import { encode } from '@ethereumjs/rlp'; + +/** + * The authorization struct as defined in EIP-7702. + * + * @property chainId - The chain ID or 0 for any chain. + * @property contractAddress - The address of the contract being authorized. + * @property nonce - The nonce of the signing account (at the time of submission). + */ +export type Authorization = [ + chainId: number, + contractAddress: string, + nonce: number, +]; + +/** + * Validates an authorization object to ensure all required parameters are present. + * + * @param authorization - The authorization object to validate. + * @throws {Error} If the authorization object or any of its required parameters are missing. + */ +function validateAuthorization(authorization: Authorization) { + if (isNullish(authorization)) { + throw new Error('Missing authorization parameter'); + } + + const [chainId, contractAddress, nonce] = authorization; + + if (isNullish(chainId)) { + throw new Error('Missing chainId parameter'); + } + + if (isNullish(contractAddress)) { + throw new Error('Missing contractAddress parameter'); + } + + if (isNullish(nonce)) { + throw new Error('Missing nonce parameter'); + } +} + +/** + * Sign an authorization message with the provided private key. + * The signature format follows EIP-155 for chain-specific signatures. + * + * @param options - The signing options. + * @param options.privateKey - The private key to sign with. + * @param options.authorization - The authorization data to sign. + * @returns The '0x'-prefixed hex encoded signature. + */ +export function signAuthorization({ + privateKey, + authorization, +}: { + privateKey: Buffer; + authorization: Authorization; +}): string { + validateAuthorization(authorization); + + if (isNullish(privateKey)) { + throw new Error('Missing privateKey parameter'); + } + + const messageHash = hashAuthorization(authorization); + + const { r, s, v } = ecsign(messageHash, privateKey); + + // v is either 27n or 28n so is guaranteed to be a single byte + const vBuffer = toBuffer(v); + + return concatSig(vBuffer, r, s); +} + +/** + * Recover the address of the account that created the given authorization + * signature. + * + * @param options - The signature recovery options. + * @param options.signature - The '0x'-prefixed hex encoded message signature. + * @param options.authorization - The authorization data that was signed. + * @returns The '0x'-prefixed hex address of the signer. + */ +export function recoverAuthorization({ + signature, + authorization, +}: { + signature: string; + authorization: Authorization; +}): string { + validateAuthorization(authorization); + + if (isNullish(signature)) { + throw new Error('Missing signature parameter'); + } + + const messageHash = hashAuthorization(authorization); + + const publicKey = recoverPublicKey(messageHash, signature); + + const sender = publicToAddress(publicKey); + + return bytesToHex(sender); +} + +/** + * Hash an authorization message according to the signing scheme. + * The message is encoded according to EIP-7702. + * + * @param authorization - The authorization data to hash. + * @returns The hash of the authorization message as a Buffer. + */ +export function hashAuthorization(authorization: Authorization): Buffer { + validateAuthorization(authorization); + + const encodedAuthorization = encode(authorization); + + const message = Buffer.concat([ + Buffer.from('05', 'hex'), + encodedAuthorization, + ]); + + return Buffer.from(keccak256(message)); +} From 6c1de88797ea04933276d37e6990449c7ca51870 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:52:21 +1300 Subject: [PATCH 2/5] Add signAuthorization components to index.ts and index.test.ts --- src/index.test.ts | 3 +++ src/index.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 60f8f417..598036d7 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -21,6 +21,9 @@ Array [ "decrypt", "decryptSafely", "getEncryptionPublicKey", + "signAuthorization", + "recoverAuthorization", + "hashAuthorization", ] `); }); diff --git a/src/index.ts b/src/index.ts index 48ddf59b..1b17ff59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './personal-sign'; export * from './sign-typed-data'; export * from './encryption'; +export * from './sign-authorization'; export { concatSig, normalize } from './utils'; From b50bc640dbfdba9b719aabcdd7d7b2785c8f4ee6 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:09:34 +1300 Subject: [PATCH 3/5] Fix linting errors --- package.json | 1 + src/sign-authorization.test.ts | 19 ++++++++----------- src/sign-authorization.ts | 3 ++- yarn.lock | 1 + 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 3728aebf..4c4385e6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "test:watch": "jest --watch" }, "dependencies": { + "@ethereumjs/rlp": "^4.0.1", "@ethereumjs/util": "^8.1.0", "@metamask/abi-utils": "^3.0.0", "@metamask/utils": "^11.0.1", diff --git a/src/sign-authorization.test.ts b/src/sign-authorization.test.ts index 66c2ebb4..bbc7d69b 100644 --- a/src/sign-authorization.test.ts +++ b/src/sign-authorization.test.ts @@ -1,4 +1,5 @@ import { bufferToHex, privateToAddress } from '@ethereumjs/util'; + import { signAuthorization, recoverAuthorization, @@ -118,7 +119,7 @@ describe('signAuthorization', () => { it('should produce the correct hash', () => { const hash = hashAuthorization(testAuthorization); - expect(hash).toEqual(expectedAuthorizationHash); + expect(hash).toStrictEqual(expectedAuthorizationHash); }); it('should throw if authorization is null', () => { @@ -213,16 +214,12 @@ describe('signAuthorization', () => { describe('sign-and-recover', () => { const testCases = { - 'zero chainId': [0, '0x1234567890123456789012345678901234567890', 1], - 'high chainId': [98765, '0x1234567890123456789012345678901234567890', 1], - 'zero nonce': [8545, '0x1234567890123456789012345678901234567890', 0], - 'high nonce': [8545, '0x1234567890123456789012345678901234567890', 98765], - 'zero contractAddress': [ - 1, - '0x0000000000000000000000000000000000000000', - 1, - ], - 'all zero values': [0, '0x0000000000000000000000000000000000000000', 0], + zeroChainId: [0, '0x1234567890123456789012345678901234567890', 1], + highChainId: [98765, '0x1234567890123456789012345678901234567890', 1], + zeroNonce: [8545, '0x1234567890123456789012345678901234567890', 0], + highNonce: [8545, '0x1234567890123456789012345678901234567890', 98765], + zeroContractAddress: [1, '0x0000000000000000000000000000000000000000', 1], + allZeroValues: [0, '0x0000000000000000000000000000000000000000', 0], } as { [key: string]: Authorization }; for (const [label, authorization] of Object.entries(testCases)) { diff --git a/src/sign-authorization.ts b/src/sign-authorization.ts index 2c0376b8..b5204051 100644 --- a/src/sign-authorization.ts +++ b/src/sign-authorization.ts @@ -1,8 +1,9 @@ +import { encode } from '@ethereumjs/rlp'; import { ecsign, publicToAddress, toBuffer } from '@ethereumjs/util'; import { bytesToHex } from '@metamask/utils'; import { keccak256 } from 'ethereum-cryptography/keccak'; + import { concatSig, isNullish, recoverPublicKey } from './utils'; -import { encode } from '@ethereumjs/rlp'; /** * The authorization struct as defined in EIP-7702. diff --git a/yarn.lock b/yarn.lock index 1ce70e50..da56d317 100644 --- a/yarn.lock +++ b/yarn.lock @@ -894,6 +894,7 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/eth-sig-util@workspace:." dependencies: + "@ethereumjs/rlp": ^4.0.1 "@ethereumjs/util": ^8.1.0 "@lavamoat/allow-scripts": ^2.3.1 "@metamask/abi-utils": ^3.0.0 From 9d7ea1bd16d0dd2defd79fbba17d9748f500533e Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:34:51 +1300 Subject: [PATCH 4/5] Remove incorrect note from comment. --- src/sign-authorization.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sign-authorization.ts b/src/sign-authorization.ts index b5204051..115d2415 100644 --- a/src/sign-authorization.ts +++ b/src/sign-authorization.ts @@ -46,7 +46,6 @@ function validateAuthorization(authorization: Authorization) { /** * Sign an authorization message with the provided private key. - * The signature format follows EIP-155 for chain-specific signatures. * * @param options - The signing options. * @param options.privateKey - The private key to sign with. From 0222c87ae104712b25a37bf027f31efa25514669 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:02:24 +1300 Subject: [PATCH 5/5] Renamed a couple of authorization symbols to explicitly be EIP-7702, shuffled non-exported members to the bottom of the file, and renamed a few test constants to aid readability. Also used it.each for multiple test cases. --- src/index.test.ts | 6 +- src/index.ts | 2 +- ....ts => sign-eip7702-authorization.test.ts} | 145 +++++++++--------- ...ation.ts => sign-eip7702-authorization.ts} | 76 ++++----- 4 files changed, 116 insertions(+), 113 deletions(-) rename src/{sign-authorization.test.ts => sign-eip7702-authorization.test.ts} (58%) rename src/{sign-authorization.ts => sign-eip7702-authorization.ts} (82%) diff --git a/src/index.test.ts b/src/index.test.ts index 598036d7..cc43e536 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -21,9 +21,9 @@ Array [ "decrypt", "decryptSafely", "getEncryptionPublicKey", - "signAuthorization", - "recoverAuthorization", - "hashAuthorization", + "signEIP7702Authorization", + "recoverEIP7702Authorization", + "hashEIP7702Authorization", ] `); }); diff --git a/src/index.ts b/src/index.ts index 1b17ff59..e1dcb4f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './personal-sign'; export * from './sign-typed-data'; export * from './encryption'; -export * from './sign-authorization'; +export * from './sign-eip7702-authorization'; export { concatSig, normalize } from './utils'; diff --git a/src/sign-authorization.test.ts b/src/sign-eip7702-authorization.test.ts similarity index 58% rename from src/sign-authorization.test.ts rename to src/sign-eip7702-authorization.test.ts index bbc7d69b..3c062197 100644 --- a/src/sign-authorization.test.ts +++ b/src/sign-eip7702-authorization.test.ts @@ -1,66 +1,66 @@ import { bufferToHex, privateToAddress } from '@ethereumjs/util'; import { - signAuthorization, - recoverAuthorization, - Authorization, - hashAuthorization, -} from './sign-authorization'; + signEIP7702Authorization, + recoverEIP7702Authorization, + EIP7702Authorization, + hashEIP7702Authorization, +} from './sign-eip7702-authorization'; -const testPrivateKey = Buffer.from( +const TEST_PRIVATE_KEY = Buffer.from( '4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0', 'hex', ); -const testAddress = bufferToHex(privateToAddress(testPrivateKey)); +const TEST_ADDRESS = bufferToHex(privateToAddress(TEST_PRIVATE_KEY)); -const testAuthorization: Authorization = [ +const TEST_AUTHORIZATION: EIP7702Authorization = [ 8545, '0x1234567890123456789012345678901234567890', 1, ]; -const expectedAuthorizationHash = Buffer.from( +const EXPECTED_AUTHORIZATION_HASH = Buffer.from( 'b847dee5b33802280f3279d57574e1eb6bf5d628d7f63049e3cb20bad211056c', 'hex', ); -const expectedSignature = +const EXPECTED_SIGNATURE = '0xebea1ac12f17a56a514dfecbcbc8bbee7b089fa3fcee31680d1e2c1588f623df7973cab74e12536678995377da38c96c65c52897750b73462c6760ef2737dba41b'; describe('signAuthorization', () => { describe('signAuthorization()', () => { it('should produce the correct signature', () => { - const signature = signAuthorization({ - privateKey: testPrivateKey, - authorization: testAuthorization, + const signature = signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, + authorization: TEST_AUTHORIZATION, }); - expect(signature).toBe(expectedSignature); + expect(signature).toBe(EXPECTED_SIGNATURE); }); it('should throw if private key is null', () => { expect(() => - signAuthorization({ + signEIP7702Authorization({ privateKey: null as any, - authorization: testAuthorization, + authorization: TEST_AUTHORIZATION, }), ).toThrow('Missing privateKey parameter'); }); it('should throw if private key is undefined', () => { expect(() => - signAuthorization({ + signEIP7702Authorization({ privateKey: undefined as any, - authorization: testAuthorization, + authorization: TEST_AUTHORIZATION, }), ).toThrow('Missing privateKey parameter'); }); it('should throw if authorization is null', () => { expect(() => - signAuthorization({ - privateKey: testPrivateKey, + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, authorization: null as any, }), ).toThrow('Missing authorization parameter'); @@ -68,8 +68,8 @@ describe('signAuthorization', () => { it('should throw if authorization is undefined', () => { expect(() => - signAuthorization({ - privateKey: testPrivateKey, + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, authorization: undefined as any, }), ).toThrow('Missing authorization parameter'); @@ -77,12 +77,12 @@ describe('signAuthorization', () => { it('should throw if chainId is null', () => { expect(() => - signAuthorization({ - privateKey: testPrivateKey, + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, authorization: [ null as unknown as number, - testAuthorization[1], - testAuthorization[2], + TEST_AUTHORIZATION[1], + TEST_AUTHORIZATION[2], ], }), ).toThrow('Missing chainId parameter'); @@ -90,12 +90,12 @@ describe('signAuthorization', () => { it('should throw if contractAddress is null', () => { expect(() => - signAuthorization({ - privateKey: testPrivateKey, + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, authorization: [ - testAuthorization[0], + TEST_AUTHORIZATION[0], null as unknown as string, - testAuthorization[2], + TEST_AUTHORIZATION[2], ], }), ).toThrow('Missing contractAddress parameter'); @@ -103,11 +103,11 @@ describe('signAuthorization', () => { it('should throw if nonce is null', () => { expect(() => - signAuthorization({ - privateKey: testPrivateKey, + signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, authorization: [ - testAuthorization[0], - testAuthorization[1], + TEST_AUTHORIZATION[0], + TEST_AUTHORIZATION[1], null as unknown as number, ], }), @@ -117,48 +117,48 @@ describe('signAuthorization', () => { describe('hashAuthorization()', () => { it('should produce the correct hash', () => { - const hash = hashAuthorization(testAuthorization); + const hash = hashEIP7702Authorization(TEST_AUTHORIZATION); - expect(hash).toStrictEqual(expectedAuthorizationHash); + expect(hash).toStrictEqual(EXPECTED_AUTHORIZATION_HASH); }); it('should throw if authorization is null', () => { - expect(() => hashAuthorization(null as unknown as Authorization)).toThrow( - 'Missing authorization parameter', - ); + expect(() => + hashEIP7702Authorization(null as unknown as EIP7702Authorization), + ).toThrow('Missing authorization parameter'); }); it('should throw if authorization is undefined', () => { expect(() => - hashAuthorization(undefined as unknown as Authorization), + hashEIP7702Authorization(undefined as unknown as EIP7702Authorization), ).toThrow('Missing authorization parameter'); }); it('should throw if chainId is null', () => { expect(() => - hashAuthorization([ + hashEIP7702Authorization([ null as unknown as number, - testAuthorization[1], - testAuthorization[2], + TEST_AUTHORIZATION[1], + TEST_AUTHORIZATION[2], ]), ).toThrow('Missing chainId parameter'); }); it('should throw if contractAddress is null', () => { expect(() => - hashAuthorization([ - testAuthorization[0], + hashEIP7702Authorization([ + TEST_AUTHORIZATION[0], null as unknown as string, - testAuthorization[2], + TEST_AUTHORIZATION[2], ]), ).toThrow('Missing contractAddress parameter'); }); it('should throw if nonce is null', () => { expect(() => - hashAuthorization([ - testAuthorization[0], - testAuthorization[1], + hashEIP7702Authorization([ + TEST_AUTHORIZATION[0], + TEST_AUTHORIZATION[1], null as unknown as number, ]), ).toThrow('Missing nonce parameter'); @@ -167,46 +167,46 @@ describe('signAuthorization', () => { describe('recoverAuthorization()', () => { it('should recover the address from a signature', () => { - const recoveredAddress = recoverAuthorization({ - authorization: testAuthorization, - signature: expectedSignature, + const recoveredAddress = recoverEIP7702Authorization({ + authorization: TEST_AUTHORIZATION, + signature: EXPECTED_SIGNATURE, }); - expect(recoveredAddress).toBe(testAddress); + expect(recoveredAddress).toBe(TEST_ADDRESS); }); it('should throw if signature is null', () => { expect(() => - recoverAuthorization({ + recoverEIP7702Authorization({ signature: null as unknown as string, - authorization: testAuthorization, + authorization: TEST_AUTHORIZATION, }), ).toThrow('Missing signature parameter'); }); it('should throw if signature is undefined', () => { expect(() => - recoverAuthorization({ + recoverEIP7702Authorization({ signature: undefined as unknown as string, - authorization: testAuthorization, + authorization: TEST_AUTHORIZATION, }), ).toThrow('Missing signature parameter'); }); it('should throw if authorization is null', () => { expect(() => - recoverAuthorization({ - signature: expectedSignature, - authorization: null as unknown as Authorization, + recoverEIP7702Authorization({ + signature: EXPECTED_SIGNATURE, + authorization: null as unknown as EIP7702Authorization, }), ).toThrow('Missing authorization parameter'); }); it('should throw if authorization is undefined', () => { expect(() => - recoverAuthorization({ - signature: expectedSignature, - authorization: undefined as unknown as Authorization, + recoverEIP7702Authorization({ + signature: EXPECTED_SIGNATURE, + authorization: undefined as unknown as EIP7702Authorization, }), ).toThrow('Missing authorization parameter'); }); @@ -220,22 +220,23 @@ describe('signAuthorization', () => { highNonce: [8545, '0x1234567890123456789012345678901234567890', 98765], zeroContractAddress: [1, '0x0000000000000000000000000000000000000000', 1], allZeroValues: [0, '0x0000000000000000000000000000000000000000', 0], - } as { [key: string]: Authorization }; + } as { [key: string]: EIP7702Authorization }; - for (const [label, authorization] of Object.entries(testCases)) { - it(`should sign and recover ${label}`, () => { - const signature = signAuthorization({ - privateKey: testPrivateKey, + it.each(Object.entries(testCases))( + 'should sign and recover %s', + (_, authorization) => { + const signature = signEIP7702Authorization({ + privateKey: TEST_PRIVATE_KEY, authorization, }); - const recoveredAddress = recoverAuthorization({ + const recoveredAddress = recoverEIP7702Authorization({ authorization, signature, }); - expect(recoveredAddress).toBe(testAddress); - }); - } + expect(recoveredAddress).toBe(TEST_ADDRESS); + }, + ); }); }); diff --git a/src/sign-authorization.ts b/src/sign-eip7702-authorization.ts similarity index 82% rename from src/sign-authorization.ts rename to src/sign-eip7702-authorization.ts index 115d2415..b73f3b3d 100644 --- a/src/sign-authorization.ts +++ b/src/sign-eip7702-authorization.ts @@ -12,38 +12,12 @@ import { concatSig, isNullish, recoverPublicKey } from './utils'; * @property contractAddress - The address of the contract being authorized. * @property nonce - The nonce of the signing account (at the time of submission). */ -export type Authorization = [ +export type EIP7702Authorization = [ chainId: number, contractAddress: string, nonce: number, ]; -/** - * Validates an authorization object to ensure all required parameters are present. - * - * @param authorization - The authorization object to validate. - * @throws {Error} If the authorization object or any of its required parameters are missing. - */ -function validateAuthorization(authorization: Authorization) { - if (isNullish(authorization)) { - throw new Error('Missing authorization parameter'); - } - - const [chainId, contractAddress, nonce] = authorization; - - if (isNullish(chainId)) { - throw new Error('Missing chainId parameter'); - } - - if (isNullish(contractAddress)) { - throw new Error('Missing contractAddress parameter'); - } - - if (isNullish(nonce)) { - throw new Error('Missing nonce parameter'); - } -} - /** * Sign an authorization message with the provided private key. * @@ -52,20 +26,20 @@ function validateAuthorization(authorization: Authorization) { * @param options.authorization - The authorization data to sign. * @returns The '0x'-prefixed hex encoded signature. */ -export function signAuthorization({ +export function signEIP7702Authorization({ privateKey, authorization, }: { privateKey: Buffer; - authorization: Authorization; + authorization: EIP7702Authorization; }): string { - validateAuthorization(authorization); + validateEIP7702Authorization(authorization); if (isNullish(privateKey)) { throw new Error('Missing privateKey parameter'); } - const messageHash = hashAuthorization(authorization); + const messageHash = hashEIP7702Authorization(authorization); const { r, s, v } = ecsign(messageHash, privateKey); @@ -84,20 +58,20 @@ export function signAuthorization({ * @param options.authorization - The authorization data that was signed. * @returns The '0x'-prefixed hex address of the signer. */ -export function recoverAuthorization({ +export function recoverEIP7702Authorization({ signature, authorization, }: { signature: string; - authorization: Authorization; + authorization: EIP7702Authorization; }): string { - validateAuthorization(authorization); + validateEIP7702Authorization(authorization); if (isNullish(signature)) { throw new Error('Missing signature parameter'); } - const messageHash = hashAuthorization(authorization); + const messageHash = hashEIP7702Authorization(authorization); const publicKey = recoverPublicKey(messageHash, signature); @@ -113,8 +87,10 @@ export function recoverAuthorization({ * @param authorization - The authorization data to hash. * @returns The hash of the authorization message as a Buffer. */ -export function hashAuthorization(authorization: Authorization): Buffer { - validateAuthorization(authorization); +export function hashEIP7702Authorization( + authorization: EIP7702Authorization, +): Buffer { + validateEIP7702Authorization(authorization); const encodedAuthorization = encode(authorization); @@ -125,3 +101,29 @@ export function hashAuthorization(authorization: Authorization): Buffer { return Buffer.from(keccak256(message)); } + +/** + * Validates an authorization object to ensure all required parameters are present. + * + * @param authorization - The authorization object to validate. + * @throws {Error} If the authorization object or any of its required parameters are missing. + */ +function validateEIP7702Authorization(authorization: EIP7702Authorization) { + if (isNullish(authorization)) { + throw new Error('Missing authorization parameter'); + } + + const [chainId, contractAddress, nonce] = authorization; + + if (isNullish(chainId)) { + throw new Error('Missing chainId parameter'); + } + + if (isNullish(contractAddress)) { + throw new Error('Missing contractAddress parameter'); + } + + if (isNullish(nonce)) { + throw new Error('Missing nonce parameter'); + } +}