Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/keyring-eth-hd/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
import {
concatSig,
decrypt,
type EIP7702Authorization,
type EthEncryptedData,
getEncryptionPublicKey,
type MessageTypes,
normalize,
personalSign,
signEIP7702Authorization,
signTypedData,
SignTypedDataVersion,
type TypedDataV1,
Expand Down Expand Up @@ -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<string> {
const privateKey = this.#getPrivateKeyFor(withAccount, opts);
return signEIP7702Authorization({
privateKey: Buffer.from(privateKey),
authorization,
});
}

/**
* Remove an account from the keyring.
*
Expand Down
58 changes: 58 additions & 0 deletions packages/keyring-eth-hd/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as oldMMForkBIP39 from '@metamask/bip39';
import {
normalize,
personalSign,
recoverEIP7702Authorization,
recoverPersonalSignature,
recoverTypedSignature,
signTypedData,
Expand All @@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down
48 changes: 48 additions & 0 deletions packages/keyring-eth-simple/src/simple-keyring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions packages/keyring-eth-simple/src/simple-keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
import {
concatSig,
decrypt,
EIP7702Authorization,
getEncryptionPublicKey,
normalize,
personalSign,
signEIP7702Authorization,
signTypedData,
SignTypedDataVersion,
} from '@metamask/eth-sig-util';
Expand Down Expand Up @@ -96,6 +98,15 @@ export default class SimpleKeyring implements Keyring<string[]> {
return signedTx ?? transaction;
}

async signEip7702Authorization(
address: Hex,
authorization: EIP7702Authorization,
opts: KeyringOpt = {},
): Promise<string> {
const privateKey = this.#getPrivateKeyFor(address, opts);
return signEIP7702Authorization({ privateKey, authorization });
}

// For eth_sign, we need to sign arbitrary data:
async signMessage(
address: Hex,
Expand Down
Loading