Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 4337 function #2

Merged
merged 10 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11,966 changes: 11,966 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@
"build": "tsc --project tsconfig.build.json",
"test": "jest --config=./test/jest.config.js"
},
"contributors": [
],
"contributors": [],
"license": "MIT",
"repository": {
"type": "git",
"url": "[email protected]:web3/web3.js-plugin-eip4337.git"
},
"dependencies": {
},
"devDependencies": {
"@chainsafe/eslint-config": "^2.0.0",
"@types/jest": "^29.5.2",
Expand All @@ -32,9 +29,11 @@
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.3",
"web3": "^4.1.1"
"web3": "^4.2.0",
"web3-eth-abi": "^4.1.3",
"web3-utils": "^4.0.7"
},
"peerDependencies": {
"web3": ">= 4.1.1"
"web3": ">= 4.2.0"
}
}
331 changes: 327 additions & 4 deletions src/index.ts

Large diffs are not rendered by default.

75 changes: 75 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
TransactionHash,
HexString32Bytes,
Uint,
Address,
Uint256,
HexStringBytes,
LogAPI,
TransactionReceiptAPI,
} from "web3";

export interface UserOperation {
sender: Address;
nonce: Uint256;
initCode: HexStringBytes;
callData: HexStringBytes;
callGasLimit?: Uint256;
verificationGasLimit: Uint256;
preVerificationGas: Uint256;
maxFeePerGas?: Uint256;
maxPriorityFeePerGas?: Uint256;
paymasterAndData: HexStringBytes;
signature: HexStringBytes;
}
export interface UserOperationRequire
extends Omit<
UserOperation,
"callGasLimit" | "maxFeePerGas" | "maxPriorityFeePerGas"
> {
callGasLimit: Uint256;
maxFeePerGas: Uint256;
maxPriorityFeePerGas: Uint256;
}

export interface IUserOperation {
readonly callData: HexStringBytes;
readonly callGasLimit: Uint;
readonly initCode: HexStringBytes;
readonly maxFeePerGas: Uint;
readonly maxPriorityFeePerGas: Uint;
readonly nonce: Uint;
readonly paymasterAndData: HexStringBytes;
readonly preVerificationGas: Uint;
readonly sender: Address;
readonly signature: HexStringBytes;
readonly verificationGasLimit: Uint;
}

export interface GetUserOperationByHashAPI {
readonly blockHash: HexString32Bytes;
readonly blockNumber: Uint;
readonly entryPoint: Address;
readonly transactionHash: TransactionHash;
readonly userOperation: IUserOperation;
}

export interface EstimateUserOperationGasAPI {
readonly preVerificationGas: Uint;
readonly verificationGasLimit: Uint;
readonly callGasLimit: Uint;
}

export interface GetUserOperationReceiptAPI {
readonly userOpHash: HexString32Bytes;
readonly entryPoint: Address;
readonly sender: Address;
readonly nonce: Uint;
readonly paymaster: Address;
readonly actualGasCost: Uint;
readonly actualGasUsed: Uint;
readonly success: boolean;
readonly reason: HexStringBytes;
readonly logs: LogAPI[];
readonly receipt: TransactionReceiptAPI;
}
52 changes: 52 additions & 0 deletions src/utils/generateUserOpHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { AbiInput, Address, Uint256, HexStringBytes } from "web3";
import { encodeParameters } from "web3-eth-abi";
import { sha3 } from "web3-utils";
import { UserOperationRequire } from "../type";

const sha3Checked = (data: string): string => {
const result = sha3(data);
if (result === undefined) {
throw new Error("sha3 returned undefined");
}
return result;
};

export const generateUserOpHash = (
userOp: UserOperationRequire,
entryPoint: string,
chainId: string
): string => {
const types: AbiInput[] = [
"address",
"uint256",
"bytes32",
"bytes32",
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
"bytes32",
];

const values: (Address | Uint256 | HexStringBytes)[] = [
userOp.sender,
userOp.nonce,
sha3Checked(userOp.initCode),
sha3Checked(userOp.callData),
userOp.callGasLimit,
userOp.verificationGasLimit,
userOp.preVerificationGas,
userOp.maxFeePerGas,
userOp.maxPriorityFeePerGas,
sha3Checked(userOp.paymasterAndData),
];

const packed: string = encodeParameters(types, values);

const enctype: AbiInput[] = ["bytes32", "address", "uint256"];
const encValues: string[] = [sha3Checked(packed), entryPoint, chainId];
const enc: string = encodeParameters(enctype, encValues);

return sha3Checked(enc);
};
14 changes: 14 additions & 0 deletions src/validator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { UserOperation } from "../type";

export type ValidInputTypes = Uint8Array | bigint | string | number | boolean;

export const isHexStrict = (hex: ValidInputTypes): boolean =>
typeof hex === "string" && /^((-)?0x[0-9a-f]+|(0x))$/i.test(hex);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may use isHexStrict from web3-utils, if you like:

Suggested change
import { UserOperation } from "../type";
export type ValidInputTypes = Uint8Array | bigint | string | number | boolean;
export const isHexStrict = (hex: ValidInputTypes): boolean =>
typeof hex === "string" && /^((-)?0x[0-9a-f]+|(0x))$/i.test(hex);
import { isHexStrict } from "web3-utils";
import { UserOperation } from "../type";
export type ValidInputTypes = Uint8Array | bigint | string | number | boolean;

Copy link
Collaborator Author

@sanyu1225 sanyu1225 Nov 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx, I noticed that the isHexStrict function from web3-utils was marked as deprecated in the documentation. So, I've switched to using the equivalent function from web3-validatorinstead. d450e29

/**
* UserOperation a full user-operation struct. All fields MUST be set as hex values. empty bytes block (e.g. empty initCode) MUST be set to "0x"
* @param userOperation - represents the structure of a transaction initiated by the user. It contains the sender, receiver, call data, maximum fee per unit of Gas, maximum priority fee, signature, nonce, and other specific elements.
* @returns boolean
*/
export const isUserOperationAllHex = (userOperation: UserOperation): boolean =>
Object.values(userOperation).every(isHexStrict);
83 changes: 64 additions & 19 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,82 @@
import Web3, { core } from "web3";
import Web3 from "web3";
import { EIP4337Plugin } from "../src";
import { UserOperation } from "../src/type";

describe("EIP4337Plugin Tests", () => {
it("should register TokensPlugin plugin on Web3Context instance", () => {
const web3Context = new core.Web3Context("http://127.0.0.1:8545");
let web3Context: Web3;
let sendSpy: jest.SpyInstance;

beforeAll(() => {
web3Context = new Web3();
web3Context.registerPlugin(new EIP4337Plugin());
expect(web3Context.EIP4337).toBeDefined();
});

it("should register TokensPlugin plugin on Web3 instance", () => {
const web3 = new Web3("http://127.0.0.1:8545");
web3.registerPlugin(new EIP4337Plugin());
expect(web3.EIP4337).toBeDefined();
expect(web3Context.EIP4337).toBeDefined();
});

describe("EIP4337Plugin method tests", () => {
let consoleSpy: jest.SpiedFunction<typeof global.console.log>;
let userOperation: UserOperation;

let web3Context: Web3;

beforeAll(() => {
web3Context = new Web3("http://127.0.0.1:8545");
web3Context.registerPlugin(new EIP4337Plugin());
consoleSpy = jest.spyOn(global.console, "log").mockImplementation();
beforeEach(() => {
userOperation = {
sender: "0x9fd042a18e90ce326073fa70f111dc9d798d9a52",
nonce: "123",
initCode: "0x68656c6c6f",
callData: "0x776F726C64",
callGasLimit: "1000",
verificationGasLimit: "2300",
preVerificationGas: "3100",
maxFeePerGas: "8500",
maxPriorityFeePerGas: "1",
paymasterAndData: "0x626c6f63746f",
signature: "0x636c656d656e74",
};
sendSpy = jest
.spyOn(web3Context.EIP4337, "sendUserOperation")
.mockImplementation();
});

afterAll(() => {
consoleSpy.mockRestore();
afterEach(() => {
sendSpy.mockRestore();
});

it("should call TempltyPlugin test method with expected param", () => {
web3Context.EIP4337.test("test-param");
expect(consoleSpy).toHaveBeenCalledWith("test-param");
it("should call rpcMethods.sendUserOperation with expected parameters", async () => {
const entryPoint = "0x636c656d656e74";
await web3Context.EIP4337.sendUserOperation(userOperation, entryPoint);
expect(sendSpy).toHaveBeenCalledWith(userOperation, entryPoint);
});

// it("should call rpcMethods.estimateUserOperationGas with expected parameters", async () => {
// const entryPoint = "0x636c656d656e74";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you will also need to add some E2E tests.

// await web3Context.EIP4337.estimateUserOperationGas(
// userOperation,
// entryPoint
// );
// expect(sendSpy).toHaveBeenCalledWith(userOperation, entryPoint);
// });

// it('should set maxFeePerGas to "0" if not provided', async () => {
// const entryPoint = "0x636c656d656e74";
// const userOperationWithoutMaxFee = {
// ...userOperation,
// maxFeePerGas: undefined,
// };
// await web3Context.EIP4337.estimateUserOperationGas(
// userOperationWithoutMaxFee,
// entryPoint
// );
// sendSpy = jest
// .spyOn(web3Context.EIP4337, "estimateUserOperationGas")
// .mockImplementation();
// expect(sendSpy).toHaveBeenCalledWith(
// web3Context.requestManager,
// expect.objectContaining({
// ...userOperationWithoutMaxFee,
// maxFeePerGas: "0", // Ensure it's set to "0"
// }),
// entryPoint
// );
// });
});
});
Loading
Loading