Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.

Commit

Permalink
feat!: add methods to support ERC-4337 accounts (#315)
Browse files Browse the repository at this point in the history
* feat: add UserOperation methods

* build: ignore errors from Snap packages

* fix: fix return type of `prepareUserOperation`

* chore: enable lcov coverage

* test: add missing unit tests

* chore: update `yarn.lock`

* chore: apply linter

* chore: update @metamask/keyring-api version to 2.0.0
  • Loading branch information
danroc authored Dec 11, 2023
1 parent ad96120 commit 16d62dd
Show file tree
Hide file tree
Showing 10 changed files with 1,895 additions and 231 deletions.
10 changes: 5 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ module.exports = {
coverageProvider: 'babel',

// A list of reporter names that Jest uses when writing coverage reports
coverageReporters: ['html', 'json-summary', 'text'],
coverageReporters: ['html', 'json-summary', 'text', 'lcov'],

// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 79.8,
functions: 93.22,
lines: 91.5,
statements: 91.69,
branches: 80.35,
functions: 93.65,
lines: 91.9,
statements: 92.07,
},
},
preset: 'ts-jest',
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@metamask/eth-hd-keyring": "^7.0.1",
"@metamask/eth-sig-util": "^7.0.0",
"@metamask/eth-simple-keyring": "^6.0.1",
"@metamask/keyring-api": "^2.0.0",
"@metamask/obs-store": "^9.0.0",
"@metamask/utils": "^8.2.0"
},
Expand All @@ -72,7 +73,7 @@
"depcheck": "^1.4.7",
"eslint": "^8.48.0",
"eslint-config-prettier": "^8.7.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-import": "~2.26.0",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsdoc": "^41",
"eslint-plugin-n": "^15.7.0",
Expand Down Expand Up @@ -100,8 +101,8 @@
"lavamoat": {
"allowScripts": {
"@lavamoat/preinstall-always-fail": false,
"@metamask/eth-hd-keyring>eth-simple-keyring>eth-sig-util>ethereumjs-util>keccak": false,
"@metamask/eth-hd-keyring>eth-simple-keyring>eth-sig-util>ethereumjs-util>secp256k1": false
"@metamask/keyring-api>@metamask/snaps-utils>@metamask/permission-controller>@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>keccak": false,
"@metamask/keyring-api>@metamask/snaps-utils>@metamask/permission-controller>@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>secp256k1": false
}
}
}
201 changes: 201 additions & 0 deletions src/KeyringController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,207 @@ describe('KeyringController', () => {
);
});

it('prepares a base UserOperation', async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
});

await keyringController.addNewKeyring('Keyring Mock With Init');
const sender = '0x998B3FBB8159aF51a827DBf43A8054A5A3A28c95';
const baseTxs = [
{
to: '0x8cBC0EA145491fe83104abA9ef916f8632367227',
value: '0x0',
data: '0x',
},
];

const baseUserOp = {
callData: '0x7064',
initCode: '0x22ff',
nonce: '0x1',
gasLimits: {
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
},
dummySignature: '0x0000',
dummyPaymasterAndData: '0x',
bundlerUrl: 'https://bundler.example.com/rpc',
};

jest
.spyOn(KeyringMockWithInit.prototype, 'getAccounts')
.mockResolvedValueOnce([sender]);

jest
.spyOn(KeyringMockWithInit.prototype, 'prepareUserOperation')
.mockResolvedValueOnce(baseUserOp);

const result = await keyringController.prepareUserOperation(
sender,
baseTxs,
);

expect(result).toStrictEqual(baseUserOp);
});

it('patches an UserOperation', async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
});

await keyringController.addNewKeyring('Keyring Mock With Init');
const sender = '0x998B3FBB8159aF51a827DBf43A8054A5A3A28c95';
const userOp = {
sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4',
nonce: '0x1',
initCode: '0x',
callData: '0x7064',
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
maxFeePerGas: '0x87f0878c0',
maxPriorityFeePerGas: '0x1dcd6500',
paymasterAndData: '0x',
signature: '0x',
};

const patch = {
paymasterAndData: '0x1234',
};

jest
.spyOn(KeyringMockWithInit.prototype, 'getAccounts')
.mockResolvedValueOnce([sender]);

jest
.spyOn(KeyringMockWithInit.prototype, 'patchUserOperation')
.mockResolvedValueOnce(patch);

const result = await keyringController.patchUserOperation(sender, userOp);
expect(result).toStrictEqual(patch);
});

it('signs an UserOperation', async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
});

await keyringController.addNewKeyring('Keyring Mock With Init');
const sender = '0x998B3FBB8159aF51a827DBf43A8054A5A3A28c95';
const userOp = {
sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4',
nonce: '0x1',
initCode: '0x',
callData: '0x7064',
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
maxFeePerGas: '0x87f0878c0',
maxPriorityFeePerGas: '0x1dcd6500',
paymasterAndData: '0x',
signature: '0x',
};

const signature = '0x1234';

jest
.spyOn(KeyringMockWithInit.prototype, 'getAccounts')
.mockResolvedValueOnce([sender]);

jest
.spyOn(KeyringMockWithInit.prototype, 'signUserOperation')
.mockResolvedValueOnce(signature);

const result = await keyringController.signUserOperation(sender, userOp);
expect(result).toStrictEqual(signature);
});

it("throws when the keyring doesn't implement prepareUserOperation", async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
seedPhrase: walletOneSeedWords,
});

const txs = [
{
to: '0x8cBC0EA145491fe83104abA9ef916f8632367227',
value: '0x0',
data: '0x',
},
];

const result = keyringController.prepareUserOperation(
walletOneAddresses[0] as string,
txs,
);

await expect(result).rejects.toThrow(
'KeyringController - The keyring for the current address does not support the method prepareUserOperation.',
);
});

it("throws when the keyring doesn't implement patchUserOperation", async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
seedPhrase: walletOneSeedWords,
});

const userOp = {
sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4',
nonce: '0x1',
initCode: '0x',
callData: '0x7064',
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
maxFeePerGas: '0x87f0878c0',
maxPriorityFeePerGas: '0x1dcd6500',
paymasterAndData: '0x',
signature: '0x',
};

const result = keyringController.patchUserOperation(
walletOneAddresses[0] as string,
userOp,
);

await expect(result).rejects.toThrow(
'KeyringController - The keyring for the current address does not support the method patchUserOperation.',
);
});

it("throws when the keyring doesn't implement signUserOperation", async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
seedPhrase: walletOneSeedWords,
});

const userOp = {
sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4',
nonce: '0x1',
initCode: '0x',
callData: '0x7064',
callGasLimit: '0x58a83',
verificationGasLimit: '0xe8c4',
preVerificationGas: '0xc57c',
maxFeePerGas: '0x87f0878c0',
maxPriorityFeePerGas: '0x1dcd6500',
paymasterAndData: '0x',
signature: '0x',
};

const result = keyringController.signUserOperation(
walletOneAddresses[0] as string,
userOp,
);

await expect(result).rejects.toThrow(
'KeyringController - The keyring for the current address does not support the method signUserOperation.',
);
});

it('signPersonalMessage', async () => {
const keyringController = await initializeKeyringController({
password: PASSWORD,
Expand Down
Loading

0 comments on commit 16d62dd

Please sign in to comment.