From 6d1538291c67d1e0c586c0f24fbe57137781f196 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:30:28 +0200 Subject: [PATCH 01/17] docs: add EIP-4337 UserOp support --- docs/architecture.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index cb297d029..c91ccd93c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -163,6 +163,11 @@ participant Site as Snap Companion Dapp User ->>+ Dapp: Create new sign request Dapp ->>+ MetaMask: ethereum.request(request) +alt Is EIP-4337 account? + MetaMask ->>+ Snap: keyring_prepareRequest(request) + Snap ->> Snap: Custom logic to prepare request + Snap -->>- MetaMask: { request } +end MetaMask ->> MetaMask: Display request to user User ->> MetaMask: Approve request @@ -208,6 +213,11 @@ participant Snap User ->>+ Dapp: Create new sign request Dapp ->>+ MetaMask: ethereum.request(request) +alt Is EIP-4337 account? + MetaMask ->>+ Snap: keyring_prepareRequest(request) + Snap ->> Snap: Custom logic to prepare request + Snap -->>- MetaMask: { request } +end MetaMask ->> MetaMask: Display request to user User ->> MetaMask: Approve request From 85f1b566506f68a5958f7afda647d3a934d7743a Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:28:27 +0100 Subject: [PATCH 02/17] wip: support 4337 accounts --- docs/evm-methods-userOp.md | 167 +++++++++++++++++++++++++++++++++++++ docs/pubilc-userOp.puml | 115 +++++++++++++++++++++++++ docs/userOp.puml | 126 ++++++++++++++++++++++++++++ src/api.ts | 9 +- src/erc4337/types.ts | 24 ++++++ 5 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 docs/evm-methods-userOp.md create mode 100644 docs/pubilc-userOp.puml create mode 100644 docs/userOp.puml create mode 100644 src/erc4337/types.ts diff --git a/docs/evm-methods-userOp.md b/docs/evm-methods-userOp.md new file mode 100644 index 000000000..3ab15b3a8 --- /dev/null +++ b/docs/evm-methods-userOp.md @@ -0,0 +1,167 @@ +# EVM Methods for ERC-4337 Accounts + +Here we document the methods that an account Snap may implement to support +requests originated from dapps. + +## eth_prepareUserOperation + +Let the keyring prepare a user operation from transaction data. + +### Parameters (Array) + +1. **Transactions (required)** + - Type: `array` + - Properties: + - Type: `object` + - Properties: + - `to` + - Type: `string` + - Pattern: `^0x[0-9a-fA-F]{40}$` + - `value` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `data` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + +### Returns + +- **UserOperation Details** + - Type: `object` + - Properties: + - `callData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `initCode` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `nonce` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `gasLimits` (optional) + - Type: `object` + - Properties + - `callGasLimit` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `verificationGasLimit` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `preVerificationGas` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `dummySignature` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `dummyPaymasterAndData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `bundler` + - Type: `string` + +### Example + +**Request:** + +```json +{ + "method": "eth_prepareUserOperation", + "params": [ + { + "to": "0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb", + "value": "0x0", + "data": "0x" + }, + { + "to": "0x660265edc169bab511a40c0e049cc1e33774443d", + "value": "0x0", + "data": "0x619a309f" + } + ] +} +``` + +**Response:** + +```json +{ + "callData": "0x70641a22000000000000000000000000963a47cc81ea17c44dbb0e101b45406dc9713b9c00000000000000000000000000000000000000000000000001dae4c156fb940000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "initCode": "0x22ff1dc5998258faa1ea45a776b57484f8ab80a2296601cd0000000000000000000000005147ce3947a407c95687131be01a2b8d55fd0a400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000007d91ea6a0bc4a4238cd72386d935e35e3d8c318400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "gasLimits": { + "callGasLimit": "21522", + "verificationGasLimit": "483316", + "preVerificationGas": "48180" + }, + "dummySignature": "0x655dfd95b0f83370672f1519cc23962b96f7a21bdd9e25be22ee46a65a6208086b27e6cb6856de3b58b295760ba45b1b866a524e9d81a9be77e221cde05430c9d63f4c7b1c", + "dummyPaymasterAndData": "0x", + "bundlerUrl": "https://bundler.example.com/rpc-endpoint" +} +``` + +## eth_patchUserOperation + +Let the keyring modify _some_ properties of a user operation. + +### Parameters (Array) + +1. UserOperation object + - Type: `object` + - Properties: + - `sender` + - Type: `string` + - Pattern: `^0x[0-9a-fA-F]{40}$` + - `nonce` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `initCode` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `callData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `callGasLimit` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `verificationGasLimit` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `preVerificationGas` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `maxFeePerGas` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `maxPriorityFeePerGas` + - Type: `string` + - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - `paymasterAndData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `signature` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + +### Returns + +- Partial UserOperation object + - Type: `object` + - Properties: + - `paymasterAndData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + +### Example + +**Request:** + +```json +{ + "method": "eth_patchUserOperation", + "params": [ + { + + } + ] +} +``` diff --git a/docs/pubilc-userOp.puml b/docs/pubilc-userOp.puml new file mode 100644 index 000000000..cf379462a --- /dev/null +++ b/docs/pubilc-userOp.puml @@ -0,0 +1,115 @@ +@startuml +autonumber +skinparam fontname Arial + +title 4337 Account Support (DRAFT) + +participant Dapp +participant MetaMask +participant Snap + +Dapp -> MetaMask ++: ""{""\n\ +"" chainId, // Ignored by MetaMask""\n\ +"" from,""\n\ +"" to,""\n\ +"" value,""\n\ +"" data,""\n\ +} + +note over MetaMask + Currently, only one transaction + per UserOp will be supported +end note + +MetaMask -> Snap ++: ""prepareUserOperation({""\n\ +"" account: account.id,""\n\ +"" scope: `eip155:${chainId}`,""\n\ +"" transactions: [ // List of transactions""\n\ +"" {""\n\ +"" to,""\n\ +"" value,""\n\ +"" data,""\n\ +"" },""\n\ +"" ]""\n\ +}) + +Snap --> MetaMask --: ""{""\n\ +"" callData,""\n\ +"" initCode?,""\n\ +"" nonce,""\n\ +"" gasLimits?: {""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" },""\n\ +"" dummySignature?,""\n\ +"" dummyPaymasterAndData?,""\n\ +"" bundler?,""\n\ +""}"" + +MetaMask -> MetaMask: Check if the account is already deployed + +alt The account is already deployed + MetaMask -> MetaMask: Remove the ""initCode"" if set +else The account is not deployed and the ""initCode"" isn't present + MetaMask -> Dapp: Throw an error (without the exact reason) +end + +alt The ""gas"" isn't set + MetaMask -> MetaMask: Estimate and set gas values +end + +MetaMask -> MetaMask: Estimate and set gas fees + +MetaMask -> Snap ++: ""patchUserOperation({""\n\ +"" sender,""\n\ +"" nonce,""\n\ +"" initCode,""\n\ +"" callData,""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" maxFeePerGas,""\n\ +"" maxPriorityFeePerGas,""\n\ +"" paymasterAndData, // Dummy value or empty""\n\ +"" signature, // Dummy value or empty""\n\ +""})"" + +Snap --> MetaMask --: ""{""\n\ +"" paymasterAndData?,""\n\ +""}"" + +MetaMask -> MetaMask: Update ""paymasterAndData"" and\n\ +remove the dummy signature + +MetaMask -> MetaMask: Display approval UI + +MetaMask -> Snap ++: ""signUserOperation([""\n\ +"" {""\n\ +"" sender,""\n\ +"" nonce,""\n\ +"" initCode,""\n\ +"" callData,""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" maxFeePerGas,""\n\ +"" maxPriorityFeePerGas,""\n\ +"" paymasterAndData,""\n\ +"" signature, // Empty""\n\ +"" },""\n\ +"" entrypoint, // Entrypoint deployed by""\n\ +"" // the Ethereum Foundation""\n\ +""])"" + +Snap --> MetaMask --: ""{""\n\ +"" signature,""\n\ +""}"" + +MetaMask -> MetaMask: Update UserOp's ""signature"" + +MetaMask -> MetaMask: Submit UserOp to bundler and\n\ +wait for transaction hash + +MetaMask --> Dapp --: ""txHash"" +@enduml diff --git a/docs/userOp.puml b/docs/userOp.puml new file mode 100644 index 000000000..88e0ce271 --- /dev/null +++ b/docs/userOp.puml @@ -0,0 +1,126 @@ +@startuml +autonumber +skinparam fontname Arial + +participant Dapp +participant UserOpController +participant AccountsController +participant Bundler +' participant KeyringController +participant Snap + +Dapp -> UserOpController ++: ""{""\n\ +"" chainId, // Ignored by MetaMask""\n\ +"" from,""\n\ +"" to,""\n\ +"" value,""\n\ +"" data,""\n\ +} + +UserOpController -> AccountsController ++: ""getAccountByAddress(tx.from)"" +AccountsController --> UserOpController --: ""account"" + +UserOpController -> Snap ++: ""buildUserOperation({""\n\ +"" account: account.id,""\n\ +"" scope: `eip155:${chainId}`,""\n\ +"" transactions: [ // List of transactions""\n\ +"" {""\n\ +"" to,""\n\ +"" value,""\n\ +"" data,""\n\ +"" },""\n\ +"" ]""\n\ +}) + +Snap --> UserOpController --: ""{""\n\ +"" callData,""\n\ +"" initCode?,""\n\ +"" nonce,""\n\ +"" gasLimits?: {""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" },""\n\ +"" dummySignature?,""\n\ +"" dummyPaymasterAndData?,""\n\ +"" bundler?,""\n\ +""}"" + +UserOpController -> UserOpController: Check if the account is already deployed + +alt The account is already deployed + UserOpController -> UserOpController: Remove the ""initCode"" if set +else The account is not deployed and the ""initCode"" isn't present + UserOpController -> Dapp: Throw an error (without the exact reason) +end + +alt The ""gas"" isn't set + UserOpController -> UserOpController: Estimate and set ""gas"" values +end + +UserOpController -> Snap ++: ""updateUserOperation({""\n\ +"" sender,""\n\ +"" nonce,""\n\ +"" initCode,""\n\ +"" callData,""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" maxFeePerGas,""\n\ +"" maxPriorityFeePerGas,""\n\ +"" paymasterAndData, // Dummy values or empty""\n\ +"" signature, // Dummy values or empty""\n\ +""})"" + +Snap --> UserOpController --: ""{""\n\ +"" paymasterAndData,""\n\ +""}"" + +UserOpController -> UserOpController: Update ""paymasterAndData"" and\n\ +remove the dummy signature + +UserOpController -> UserOpController: Display approval UI + +UserOpController -> Snap ++: ""signUserOperation({""\n\ +"" sender,""\n\ +"" nonce,""\n\ +"" initCode,""\n\ +"" callData,""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" maxFeePerGas,""\n\ +"" maxPriorityFeePerGas,""\n\ +"" paymasterAndData,""\n\ +"" signature, // Empty""\n\ +""})"" + +Snap --> UserOpController --: ""{""\n\ +"" signature,""\n\ +""}"" + +UserOpController -> UserOpController: Update ""signature"" + +UserOpController -> Bundler ++: ""eth_sendUserOperation([""\n\ +"" {""\n\ +"" sender,""\n\ +"" nonce,""\n\ +"" initCode,""\n\ +"" callData,""\n\ +"" callGasLimit,""\n\ +"" verificationGasLimit,""\n\ +"" preVerificationGas,""\n\ +"" maxFeePerGas,""\n\ +"" maxPriorityFeePerGas,""\n\ +"" paymasterAndData,""\n\ +"" signature,""\n\ +"" },""\n\ +"" entrypoint,""\n\ +""])"" + +Bundler --> UserOpController --: ""userOpHash"" + +UserOpController -> UserOpController: Wait until the bundle is submitted + +UserOpController --> Dapp --: ""txHash"" +@enduml diff --git a/src/api.ts b/src/api.ts index 915469a6a..5a5b4e1c6 100644 --- a/src/api.ts +++ b/src/api.ts @@ -10,12 +10,17 @@ import { UuidStruct } from './utils'; * Supported Ethereum methods. */ export enum EthMethod { + // General signing methods PersonalSign = 'personal_sign', Sign = 'eth_sign', SignTransaction = 'eth_signTransaction', SignTypedDataV1 = 'eth_signTypedData_v1', SignTypedDataV3 = 'eth_signTypedData_v3', SignTypedDataV4 = 'eth_signTypedData_v4', + // ERC-4337 methods + PrepareUserOperation = 'eth_prepareUserOperation', + PatchUserOperation = 'eth_patchUserOperation', + SignUserOperation = 'eth_signUserOperation', } /** @@ -23,7 +28,7 @@ export enum EthMethod { */ export enum EthAccountType { Eoa = 'eip155:eoa', - Eip4337 = 'eip155:eip4337', + Erc4337 = 'eip155:erc4337', } export const KeyringAccountStruct = object({ @@ -59,7 +64,7 @@ export const KeyringAccountStruct = object({ /** * Account type. */ - type: enums([`${EthAccountType.Eoa}`, `${EthAccountType.Eip4337}`]), + type: enums([`${EthAccountType.Eoa}`, `${EthAccountType.Erc4337}`]), }); /** diff --git a/src/erc4337/types.ts b/src/erc4337/types.ts new file mode 100644 index 000000000..245b88126 --- /dev/null +++ b/src/erc4337/types.ts @@ -0,0 +1,24 @@ +import type { Infer } from 'superstruct'; +import { string } from 'superstruct'; + +import { object } from '../superstruct'; + +/** + * Struct of a UserOperation as defined by ERC-4337. + * @see https://eips.ethereum.org/EIPS/eip-4337#definitions + */ +export const EthUserOperationStruct = object({ + sender: string(), + nonce: string(), + initCode: string(), + callData: string(), + callGasLimit: string(), + verificationGasLimit: string(), + preVerificationGas: string(), + maxFeePerGas: string(), + maxPriorityFeePerGas: string(), + paymasterAndData: string(), + signature: string(), +}); + +export type EthUserOperation = Infer; From 70161e206dd3195dcd40fce23b609ed676fbfefd Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:49:16 +0100 Subject: [PATCH 03/17] chore: simplify regex --- docs/evm-methods.md | 14 ++++++------ ...ethods-userOp.md => evm_methods_userOp.md} | 22 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) rename docs/{evm-methods-userOp.md => evm_methods_userOp.md} (88%) diff --git a/docs/evm-methods.md b/docs/evm-methods.md index 554ebc600..d332664a1 100644 --- a/docs/evm-methods.md +++ b/docs/evm-methods.md @@ -105,7 +105,7 @@ Adds support to [`eth_sendTransaction`][eth-send-transaction]. - Pattern: `^0x[0-9a-fA-F]{1,2}$` - `nonce` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `to` - One-of: - Contract creation @@ -118,22 +118,22 @@ Adds support to [`eth_sendTransaction`][eth-send-transaction]. - Pattern: `^0x[0-9a-fA-F]{40}$` - `value` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `data` - Type: `string` - Pattern: `^0x[0-9a-f]*$` - `gasLimit` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `gasPrice` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `maxPriorityFeePerGas` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `maxFeePerGas` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `accessList`: - Description: EIP-2930 access list - Type: `array` @@ -150,7 +150,7 @@ Adds support to [`eth_sendTransaction`][eth-send-transaction]. - Pattern: `^0x[0-9a-f]{64}$` - `chainId` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` ### Returns diff --git a/docs/evm-methods-userOp.md b/docs/evm_methods_userOp.md similarity index 88% rename from docs/evm-methods-userOp.md rename to docs/evm_methods_userOp.md index 3ab15b3a8..9382bbf25 100644 --- a/docs/evm-methods-userOp.md +++ b/docs/evm_methods_userOp.md @@ -19,7 +19,7 @@ Let the keyring prepare a user operation from transaction data. - Pattern: `^0x[0-9a-fA-F]{40}$` - `value` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `data` - Type: `string` - Pattern: `^0x[0-9a-f]*$` @@ -37,19 +37,19 @@ Let the keyring prepare a user operation from transaction data. - Pattern: `^0x[0-9a-f]*$` - `nonce` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `gasLimits` (optional) - Type: `object` - Properties - `callGasLimit` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `verificationGasLimit` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `preVerificationGas` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `dummySignature` - Type: `string` - Pattern: `^0x[0-9a-f]*$` @@ -113,7 +113,7 @@ Let the keyring modify _some_ properties of a user operation. - Pattern: `^0x[0-9a-fA-F]{40}$` - `nonce` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `initCode` - Type: `string` - Pattern: `^0x[0-9a-f]*$` @@ -122,19 +122,19 @@ Let the keyring modify _some_ properties of a user operation. - Pattern: `^0x[0-9a-f]*$` - `callGasLimit` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `verificationGasLimit` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `preVerificationGas` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `maxFeePerGas` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `maxPriorityFeePerGas` - Type: `string` - - Pattern: `^0x([1-9a-f]+[0-9a-f]*|0)$` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` - `paymasterAndData` - Type: `string` - Pattern: `^0x[0-9a-f]*$` From ebe91f584edf67f3edb5b804d432905f3393a513 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:49:41 +0100 Subject: [PATCH 04/17] feat: add `definePattern` helper function --- src/superstruct.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/superstruct.ts b/src/superstruct.ts index 002b67ca3..a3c6e0c1f 100644 --- a/src/superstruct.ts +++ b/src/superstruct.ts @@ -1,5 +1,5 @@ import type { Infer, Context } from 'superstruct'; -import { Struct, object as stObject } from 'superstruct'; +import { Struct, define, object as stObject } from 'superstruct'; import type { ObjectSchema, OmitBy, @@ -101,3 +101,27 @@ export function exactOptional( !hasOptional(ctx) || struct.refiner(value as Type, ctx), }); } + +/** + * Defines a new string-struct matching a regular expression. + * + * Example: + * + * ```ts + * const EthAddressStruct = definePattern('EthAddress', /^0x[0-9a-f]{40}$/iu); + * ``` + * + * @param name - Type name. + * @param pattern - Regular expression to match. + * @returns A new string-struct that matches the given pattern. + */ +export function definePattern( + name: string, + pattern: RegExp, +): Struct { + return define( + name, + (value: unknown): boolean => + typeof value === 'string' && pattern.test(value), + ); +} From 2fe41868a8b4839a5cc5dc17cc4f7d486eb4f5f7 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:50:29 +0100 Subject: [PATCH 05/17] chore: move types to `eth` directory --- src/erc4337/types.ts | 24 ---------- src/eth/erc4337/types.test-d.ts | 46 +++++++++++++++++++ src/eth/erc4337/types.test.ts | 79 +++++++++++++++++++++++++++++++++ src/eth/erc4337/types.ts | 24 ++++++++++ src/eth/types.ts | 13 ++++++ 5 files changed, 162 insertions(+), 24 deletions(-) delete mode 100644 src/erc4337/types.ts create mode 100644 src/eth/erc4337/types.test-d.ts create mode 100644 src/eth/erc4337/types.test.ts create mode 100644 src/eth/erc4337/types.ts create mode 100644 src/eth/types.ts diff --git a/src/erc4337/types.ts b/src/erc4337/types.ts deleted file mode 100644 index 245b88126..000000000 --- a/src/erc4337/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Infer } from 'superstruct'; -import { string } from 'superstruct'; - -import { object } from '../superstruct'; - -/** - * Struct of a UserOperation as defined by ERC-4337. - * @see https://eips.ethereum.org/EIPS/eip-4337#definitions - */ -export const EthUserOperationStruct = object({ - sender: string(), - nonce: string(), - initCode: string(), - callData: string(), - callGasLimit: string(), - verificationGasLimit: string(), - preVerificationGas: string(), - maxFeePerGas: string(), - maxPriorityFeePerGas: string(), - paymasterAndData: string(), - signature: string(), -}); - -export type EthUserOperation = Infer; diff --git a/src/eth/erc4337/types.test-d.ts b/src/eth/erc4337/types.test-d.ts new file mode 100644 index 000000000..ddc58c6c1 --- /dev/null +++ b/src/eth/erc4337/types.test-d.ts @@ -0,0 +1,46 @@ +import { expectAssignable, expectNotAssignable } from 'tsd'; + +import type { EthUserOperation } from './types'; + +// Valid UserOperation +expectAssignable({ + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a', + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + paymasterAndData: '0x1234', + signature: '0x1234', +}); + +// Missing `paymasterAndData` property +expectNotAssignable({ + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a', + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + signature: '0x1234', +}); + +// Missing `signature` property +expectNotAssignable({ + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a', + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + paymasterAndData: '0x1234', +}); diff --git a/src/eth/erc4337/types.test.ts b/src/eth/erc4337/types.test.ts new file mode 100644 index 000000000..50175e498 --- /dev/null +++ b/src/eth/erc4337/types.test.ts @@ -0,0 +1,79 @@ +import { assert } from 'superstruct'; + +import { EthUserOperationStruct } from './types'; + +describe('types', () => { + it('is a valid UserOperation', () => { + const userOp = { + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a', + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + paymasterAndData: '0x1234', + signature: '0x1234', + }; + expect(() => assert(userOp, EthUserOperationStruct)).not.toThrow(); + }); + + it('has an shorter sender address', () => { + const userOp = { + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb2', + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + paymasterAndData: '0x1234', + signature: '0x1234', + }; + expect(() => assert(userOp, EthUserOperationStruct)).toThrow( + 'At path: sender -- Expected a value of type `EthAddress`, but received: `"0x2A3e54af44480ad269cca53e3a4d90ce2DbEb2"`', + ); + }); + + it('has an longer sender address', () => { + const userOp = { + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a00', + nonce: '0x1', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + paymasterAndData: '0x1234', + signature: '0x1234', + }; + expect(() => assert(userOp, EthUserOperationStruct)).toThrow( + 'At path: sender -- Expected a value of type `EthAddress`, but received: `"0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a00"`', + ); + }); + + it('has an nonce that starts with zero', () => { + const userOp = { + sender: '0x2A3e54af44480ad269cca53e3a4d90ce2DbEb23a', + nonce: '0x01', + initCode: '0x', + callData: '0x70641a22000000000000000000000000', + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + maxFeePerGas: '0x87f0878c0', + maxPriorityFeePerGas: '0x1dcd6500', + paymasterAndData: '0x1234', + signature: '0x1234', + }; + expect(() => assert(userOp, EthUserOperationStruct)).toThrow( + 'At path: nonce -- Expected a value of type `EthUint256`, but received: `"0x01"`', + ); + }); +}); diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts new file mode 100644 index 000000000..45d0c2143 --- /dev/null +++ b/src/eth/erc4337/types.ts @@ -0,0 +1,24 @@ +import type { Infer } from 'superstruct'; + +import { object } from '../../superstruct'; +import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types'; + +/** + * Struct of a UserOperation as defined by ERC-4337. + * @see https://eips.ethereum.org/EIPS/eip-4337#definitions + */ +export const EthUserOperationStruct = object({ + sender: EthAddressStruct, + nonce: EthUint256Struct, + initCode: EthBytesStruct, + callData: EthBytesStruct, + callGasLimit: EthUint256Struct, + verificationGasLimit: EthUint256Struct, + preVerificationGas: EthUint256Struct, + maxFeePerGas: EthUint256Struct, + maxPriorityFeePerGas: EthUint256Struct, + paymasterAndData: EthBytesStruct, + signature: EthBytesStruct, +}); + +export type EthUserOperation = Infer; diff --git a/src/eth/types.ts b/src/eth/types.ts new file mode 100644 index 000000000..921419764 --- /dev/null +++ b/src/eth/types.ts @@ -0,0 +1,13 @@ +import { definePattern } from '../superstruct'; + +export const EthBytesStruct = definePattern('EthBytes', /^0x[0-9a-f]*$/iu); + +export const EthAddressStruct = definePattern( + 'EthAddress', + /^0x[0-9a-f]{40}$/iu, +); + +export const EthUint256Struct = definePattern( + 'EthUint256', + /^0x([1-9a-f][0-9a-f]*|0)$/iu, +); From 346ab7d3561895a1ebd71a8ceaf7eaec34f9ef66 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:50:51 +0100 Subject: [PATCH 06/17] chore: use `definePattern` to define `UuidV4` type --- src/utils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 75d22dd4d..7e4393c2f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,15 +1,14 @@ -import { assert, define } from 'superstruct'; +import { assert } from 'superstruct'; import type { Struct } from 'superstruct'; -const UUID_V4_REGEX = - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; +import { definePattern } from './superstruct'; /** * UUIDv4 struct. */ -export const UuidStruct = define( +export const UuidStruct = definePattern( 'UuidV4', - (id: unknown): boolean => typeof id === 'string' && UUID_V4_REGEX.test(id), + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu, ); /** From 3febcb6d9ceb537ce919d25e67e417c0676a75ab Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:51:12 +0100 Subject: [PATCH 07/17] chore: move user operation sequence diagram --- .../user_operation_signing.puml} | 4 +- docs/userOp.puml | 126 ------------------ 2 files changed, 2 insertions(+), 128 deletions(-) rename docs/{pubilc-userOp.puml => diagrams/user_operation_signing.puml} (97%) delete mode 100644 docs/userOp.puml diff --git a/docs/pubilc-userOp.puml b/docs/diagrams/user_operation_signing.puml similarity index 97% rename from docs/pubilc-userOp.puml rename to docs/diagrams/user_operation_signing.puml index cf379462a..9987446cf 100644 --- a/docs/pubilc-userOp.puml +++ b/docs/diagrams/user_operation_signing.puml @@ -1,8 +1,8 @@ -@startuml +@startuml "ERC-4337 Account Support" autonumber skinparam fontname Arial -title 4337 Account Support (DRAFT) +title "ERC-4337 UserOperation Signing" participant Dapp participant MetaMask diff --git a/docs/userOp.puml b/docs/userOp.puml deleted file mode 100644 index 88e0ce271..000000000 --- a/docs/userOp.puml +++ /dev/null @@ -1,126 +0,0 @@ -@startuml -autonumber -skinparam fontname Arial - -participant Dapp -participant UserOpController -participant AccountsController -participant Bundler -' participant KeyringController -participant Snap - -Dapp -> UserOpController ++: ""{""\n\ -"" chainId, // Ignored by MetaMask""\n\ -"" from,""\n\ -"" to,""\n\ -"" value,""\n\ -"" data,""\n\ -} - -UserOpController -> AccountsController ++: ""getAccountByAddress(tx.from)"" -AccountsController --> UserOpController --: ""account"" - -UserOpController -> Snap ++: ""buildUserOperation({""\n\ -"" account: account.id,""\n\ -"" scope: `eip155:${chainId}`,""\n\ -"" transactions: [ // List of transactions""\n\ -"" {""\n\ -"" to,""\n\ -"" value,""\n\ -"" data,""\n\ -"" },""\n\ -"" ]""\n\ -}) - -Snap --> UserOpController --: ""{""\n\ -"" callData,""\n\ -"" initCode?,""\n\ -"" nonce,""\n\ -"" gasLimits?: {""\n\ -"" callGasLimit,""\n\ -"" verificationGasLimit,""\n\ -"" preVerificationGas,""\n\ -"" },""\n\ -"" dummySignature?,""\n\ -"" dummyPaymasterAndData?,""\n\ -"" bundler?,""\n\ -""}"" - -UserOpController -> UserOpController: Check if the account is already deployed - -alt The account is already deployed - UserOpController -> UserOpController: Remove the ""initCode"" if set -else The account is not deployed and the ""initCode"" isn't present - UserOpController -> Dapp: Throw an error (without the exact reason) -end - -alt The ""gas"" isn't set - UserOpController -> UserOpController: Estimate and set ""gas"" values -end - -UserOpController -> Snap ++: ""updateUserOperation({""\n\ -"" sender,""\n\ -"" nonce,""\n\ -"" initCode,""\n\ -"" callData,""\n\ -"" callGasLimit,""\n\ -"" verificationGasLimit,""\n\ -"" preVerificationGas,""\n\ -"" maxFeePerGas,""\n\ -"" maxPriorityFeePerGas,""\n\ -"" paymasterAndData, // Dummy values or empty""\n\ -"" signature, // Dummy values or empty""\n\ -""})"" - -Snap --> UserOpController --: ""{""\n\ -"" paymasterAndData,""\n\ -""}"" - -UserOpController -> UserOpController: Update ""paymasterAndData"" and\n\ -remove the dummy signature - -UserOpController -> UserOpController: Display approval UI - -UserOpController -> Snap ++: ""signUserOperation({""\n\ -"" sender,""\n\ -"" nonce,""\n\ -"" initCode,""\n\ -"" callData,""\n\ -"" callGasLimit,""\n\ -"" verificationGasLimit,""\n\ -"" preVerificationGas,""\n\ -"" maxFeePerGas,""\n\ -"" maxPriorityFeePerGas,""\n\ -"" paymasterAndData,""\n\ -"" signature, // Empty""\n\ -""})"" - -Snap --> UserOpController --: ""{""\n\ -"" signature,""\n\ -""}"" - -UserOpController -> UserOpController: Update ""signature"" - -UserOpController -> Bundler ++: ""eth_sendUserOperation([""\n\ -"" {""\n\ -"" sender,""\n\ -"" nonce,""\n\ -"" initCode,""\n\ -"" callData,""\n\ -"" callGasLimit,""\n\ -"" verificationGasLimit,""\n\ -"" preVerificationGas,""\n\ -"" maxFeePerGas,""\n\ -"" maxPriorityFeePerGas,""\n\ -"" paymasterAndData,""\n\ -"" signature,""\n\ -"" },""\n\ -"" entrypoint,""\n\ -""])"" - -Bundler --> UserOpController --: ""userOpHash"" - -UserOpController -> UserOpController: Wait until the bundle is submitted - -UserOpController --> Dapp --: ""txHash"" -@enduml From 3fbdab21693067555fc0ce2ac718e4c18a28358c Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:57:49 +0100 Subject: [PATCH 08/17] docs: add `eth_signUserOperation` --- docs/evm_methods_userOp.md | 128 +++++++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 13 deletions(-) diff --git a/docs/evm_methods_userOp.md b/docs/evm_methods_userOp.md index 9382bbf25..d97faa4b6 100644 --- a/docs/evm_methods_userOp.md +++ b/docs/evm_methods_userOp.md @@ -5,7 +5,7 @@ requests originated from dapps. ## eth_prepareUserOperation -Let the keyring prepare a user operation from transaction data. +Prepare a new UserOperation from transaction data. ### Parameters (Array) @@ -85,27 +85,27 @@ Let the keyring prepare a user operation from transaction data. ```json { - "callData": "0x70641a22000000000000000000000000963a47cc81ea17c44dbb0e101b45406dc9713b9c00000000000000000000000000000000000000000000000001dae4c156fb940000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "callData": "0x70641a22000000000000000000000000f3de3c0d654fda23dad170f0f320a921725091270000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e49871efa4000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000067fd192000000000000000000000000000000000000000001411a0c3b763237f484fdd70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000280000000000000003b6d03400d4a11d5eeaac28ec3f61d100daf4d40471f185280000000000000003b6d03408f1b19622a888c53c8ee4f7d7b4dc8f574ff906800000000000000000000000000000000000000000000000000000000", "initCode": "0x22ff1dc5998258faa1ea45a776b57484f8ab80a2296601cd0000000000000000000000005147ce3947a407c95687131be01a2b8d55fd0a400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000007d91ea6a0bc4a4238cd72386d935e35e3d8c318400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", - "nonce": "0x0", + "nonce": "0x1", "gasLimits": { - "callGasLimit": "21522", - "verificationGasLimit": "483316", - "preVerificationGas": "48180" + "callGasLimit": "0x58a83", + "verificationGasLimit": "0xe8c4", + "preVerificationGas": "0xc57c" }, - "dummySignature": "0x655dfd95b0f83370672f1519cc23962b96f7a21bdd9e25be22ee46a65a6208086b27e6cb6856de3b58b295760ba45b1b866a524e9d81a9be77e221cde05430c9d63f4c7b1c", - "dummyPaymasterAndData": "0x", - "bundlerUrl": "https://bundler.example.com/rpc-endpoint" + "dummySignature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "dummyPaymasterAndData": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "bundlerUrl": "https://bundler.example.com/rpc" } ``` ## eth_patchUserOperation -Let the keyring modify _some_ properties of a user operation. +Patch _some_ allowed properties of an UserOperation. ### Parameters (Array) -1. UserOperation object +1. **UserOperation object** - Type: `object` - Properties: - `sender` @@ -144,7 +144,7 @@ Let the keyring modify _some_ properties of a user operation. ### Returns -- Partial UserOperation object +- **Partial UserOperation object** - Type: `object` - Properties: - `paymasterAndData` @@ -160,8 +160,110 @@ Let the keyring modify _some_ properties of a user operation. "method": "eth_patchUserOperation", "params": [ { - + "sender": "0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4", + "nonce": "0x1", + "initCode": "0x", + "callData": "0x70641a22000000000000000000000000f3de3c0d654fda23dad170f0f320a921725091270000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e49871efa4000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000067fd192000000000000000000000000000000000000000001411a0c3b763237f484fdd70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000280000000000000003b6d03400d4a11d5eeaac28ec3f61d100daf4d40471f185280000000000000003b6d03408f1b19622a888c53c8ee4f7d7b4dc8f574ff906800000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0x58a83", + "verificationGasLimit": "0xe8c4", + "preVerificationGas": "0xc57c", + "maxFeePerGas": "0x87f0878c0", + "maxPriorityFeePerGas": "0x1dcd6500", + "paymasterAndData": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" } ] } ``` + +**Response:** + +```json +{ + "paymasterAndData": "0x952514d7cBCB495EACeB86e02154921401dB0Cd9dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000779b3fbb00000000000000006565b267000000000000000000000000000000000000000029195b31a9b1c6ccdeff53e359ebbcd5f075a93c1aaed93302e5fde5faf8047028b296b8a3fa4e22b063af5069ae9f656736ffda0ee090c0311155722b905f781b" +} +``` + +## eth_signUserOperation + +Sign an UserOperation. + +### Parameters (Array) + +1. **UserOperation object** + - Type: `object` + - Properties: + - `sender` + - Type: `string` + - Pattern: `^0x[0-9a-fA-F]{40}$` + - `nonce` + - Type: `string` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` + - `initCode` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `callData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `callGasLimit` + - Type: `string` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` + - `verificationGasLimit` + - Type: `string` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` + - `preVerificationGas` + - Type: `string` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` + - `maxFeePerGas` + - Type: `string` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` + - `maxPriorityFeePerGas` + - Type: `string` + - Pattern: `^0x([1-9a-f][0-9a-f]*|0)$` + - `paymasterAndData` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + - `signature` + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` +2. **Entrypoint** + - Type: `string` + - Pattern: `^0x[0-9a-f]{40}$` + +### Returns + +- **Signature** + - Type: `string` + - Pattern: `^0x[0-9a-f]*$` + +### Example + +**Request:** + +```json +{ + "method": "eth_signUserOperation", + "params": [ + { + "sender": "0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4", + "nonce": "0x1", + "initCode": "0x", + "callData": "0x70641a22000000000000000000000000f3de3c0d654fda23dad170f0f320a921725091270000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e49871efa4000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000067fd192000000000000000000000000000000000000000001411a0c3b763237f484fdd70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000280000000000000003b6d03400d4a11d5eeaac28ec3f61d100daf4d40471f185280000000000000003b6d03408f1b19622a888c53c8ee4f7d7b4dc8f574ff906800000000000000000000000000000000000000000000000000000000", + "callGasLimit": "0x58a83", + "verificationGasLimit": "0xe8c4", + "preVerificationGas": "0xc57c", + "maxFeePerGas": "0x87f0878c0", + "maxPriorityFeePerGas": "0x1dcd6500", + "paymasterAndData": "0x952514d7cBCB495EACeB86e02154921401dB0Cd9dac17f958d2ee523a2206206994597c13d831ec700000000000000000000000000000000000000000000000000000000779b3fbb00000000000000006565b267000000000000000000000000000000000000000029195b31a9b1c6ccdeff53e359ebbcd5f075a93c1aaed93302e5fde5faf8047028b296b8a3fa4e22b063af5069ae9f656736ffda0ee090c0311155722b905f781b", + "signature": "0x" + }, + "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" + ] +} +``` + +**Response:** + +```json +"0x6565acc7efd3c85e4c0c221c2958ff6c3ae68401b23b33fdcd1a2d49034c30d97b1cfa17487b90253a5dfd54ef5188688592c2fd56ba44ee4d948ea259d636cd550f6dd21b" +``` From 1c3855591a6e14fdfb15869c669e5f0778f074b4 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:01:05 +0100 Subject: [PATCH 09/17] feat: add `BasicTransaction` type --- src/eth/erc4337/types.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts index 45d0c2143..667f24214 100644 --- a/src/eth/erc4337/types.ts +++ b/src/eth/erc4337/types.ts @@ -22,3 +22,26 @@ export const EthUserOperationStruct = object({ }); export type EthUserOperation = Infer; + +/** + * Struct containing the most basic transaction information required to + * construct a UserOperation. + */ +export const EthBasicTransactionStruct = object({ + /** + * Address of the transaction recipient. + */ + to: EthAddressStruct, + + /** + * Amount of wei to transfer to the recipient. + */ + value: EthUint256Struct, + + /** + * Data to pass to the recipient. + */ + data: EthBytesStruct, +}); + +export type EthBasicTransaction = Infer; From cad21731cae17a2f8b6d4231110a04f21dc5007a Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:04:19 +0100 Subject: [PATCH 10/17] fix: add index files --- src/eth/erc4337/index.ts | 1 + src/eth/index.ts | 1 + src/index.ts | 1 + 3 files changed, 3 insertions(+) create mode 100644 src/eth/erc4337/index.ts create mode 100644 src/eth/index.ts diff --git a/src/eth/erc4337/index.ts b/src/eth/erc4337/index.ts new file mode 100644 index 000000000..fcb073fef --- /dev/null +++ b/src/eth/erc4337/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/src/eth/index.ts b/src/eth/index.ts new file mode 100644 index 000000000..291328044 --- /dev/null +++ b/src/eth/index.ts @@ -0,0 +1 @@ +export * from './erc4337'; diff --git a/src/index.ts b/src/index.ts index 491107036..7aa2fbbd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './api'; +export * from './eth'; export * from './events'; export * from './internal'; export * from './KeyringClient'; From 5c8ba83b74cf0f21a1a5ed7e9adfe0b2c2f627ea Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:06:09 +0100 Subject: [PATCH 11/17] feat: add `PreparedUserOperation` type --- docs/evm_methods_userOp.md | 2 +- src/eth/erc4337/types.ts | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/evm_methods_userOp.md b/docs/evm_methods_userOp.md index d97faa4b6..3c8f69703 100644 --- a/docs/evm_methods_userOp.md +++ b/docs/evm_methods_userOp.md @@ -56,7 +56,7 @@ Prepare a new UserOperation from transaction data. - `dummyPaymasterAndData` - Type: `string` - Pattern: `^0x[0-9a-f]*$` - - `bundler` + - `bundlerUrl` - Type: `string` ### Example diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts index 667f24214..ba6e021b9 100644 --- a/src/eth/erc4337/types.ts +++ b/src/eth/erc4337/types.ts @@ -1,4 +1,4 @@ -import type { Infer } from 'superstruct'; +import { string, type Infer } from 'superstruct'; import { object } from '../../superstruct'; import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types'; @@ -45,3 +45,21 @@ export const EthBasicTransactionStruct = object({ }); export type EthBasicTransaction = Infer; + +export const EthPreparedUserOperationStruct = object({ + callData: EthBytesStruct, + initCode: EthBytesStruct, + nonce: EthUint256Struct, + gasLimits: object({ + callGasLimit: EthUint256Struct, + verificationGasLimit: EthUint256Struct, + preVerificationGas: EthUint256Struct, + }), + dummySignature: EthBytesStruct, + dummyPaymasterAndData: EthBytesStruct, + bundlerUrl: string(), +}); + +export type EthPreparedUserOperation = Infer< + typeof EthPreparedUserOperationStruct +>; From b8c556d8a05d24940e78434555df23fda85d51d2 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:53:18 +0100 Subject: [PATCH 12/17] fix: make `gasLimits` optional --- src/eth/erc4337/types.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts index ba6e021b9..94928fbad 100644 --- a/src/eth/erc4337/types.ts +++ b/src/eth/erc4337/types.ts @@ -1,6 +1,6 @@ import { string, type Infer } from 'superstruct'; -import { object } from '../../superstruct'; +import { exactOptional, object } from '../../superstruct'; import { EthAddressStruct, EthBytesStruct, EthUint256Struct } from '../types'; /** @@ -27,7 +27,7 @@ export type EthUserOperation = Infer; * Struct containing the most basic transaction information required to * construct a UserOperation. */ -export const EthBasicTransactionStruct = object({ +export const EthBaseTransactionStruct = object({ /** * Address of the transaction recipient. */ @@ -44,22 +44,22 @@ export const EthBasicTransactionStruct = object({ data: EthBytesStruct, }); -export type EthBasicTransaction = Infer; +export type EthBaseTransaction = Infer; -export const EthPreparedUserOperationStruct = object({ - callData: EthBytesStruct, - initCode: EthBytesStruct, +export const EthBaseUserOperationStruct = object({ nonce: EthUint256Struct, - gasLimits: object({ - callGasLimit: EthUint256Struct, - verificationGasLimit: EthUint256Struct, - preVerificationGas: EthUint256Struct, - }), - dummySignature: EthBytesStruct, + initCode: EthBytesStruct, + callData: EthBytesStruct, + gasLimits: exactOptional( + object({ + callGasLimit: EthUint256Struct, + verificationGasLimit: EthUint256Struct, + preVerificationGas: EthUint256Struct, + }), + ), dummyPaymasterAndData: EthBytesStruct, + dummySignature: EthBytesStruct, bundlerUrl: string(), }); -export type EthPreparedUserOperation = Infer< - typeof EthPreparedUserOperationStruct ->; +export type EthBaseUserOperation = Infer; From 7513850cac9d57d7274174bb923187978df13b20 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:27:58 +0100 Subject: [PATCH 13/17] feat: add `EthUserOperationPatch` type --- src/eth/erc4337/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/eth/erc4337/types.ts b/src/eth/erc4337/types.ts index 94928fbad..4bd3f3c59 100644 --- a/src/eth/erc4337/types.ts +++ b/src/eth/erc4337/types.ts @@ -63,3 +63,9 @@ export const EthBaseUserOperationStruct = object({ }); export type EthBaseUserOperation = Infer; + +export const EthUserOperationPatchStruct = object({ + paymasterAndData: EthBytesStruct, +}); + +export type EthUserOperationPatch = Infer; From 1a7f3562a855a9ae50207a91a940705c4eab754a Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:30:42 +0100 Subject: [PATCH 14/17] feat: export base ETH types --- src/eth/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/eth/index.ts b/src/eth/index.ts index 291328044..a7e4b147d 100644 --- a/src/eth/index.ts +++ b/src/eth/index.ts @@ -1 +1,2 @@ export * from './erc4337'; +export * from './types'; From 67ffc9aa68526c7475889e8b551adf0bd7120cf8 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:47:23 +0100 Subject: [PATCH 15/17] feat: add `EthKeyring` type --- jest.config.js | 6 ++++- src/internal/eth/EthKeyring.ts | 47 ++++++++++++++++++++++++++++++++++ src/internal/eth/index.ts | 1 + src/internal/index.ts | 1 + 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/internal/eth/EthKeyring.ts create mode 100644 src/internal/eth/index.ts diff --git a/jest.config.js b/jest.config.js index cef3c14f9..bd7fddb65 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,7 +22,11 @@ module.exports = { collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['./src/**/*.ts', '!./src/**/*.test-d.ts'], + collectCoverageFrom: [ + './src/**/*.ts', + '!./src/**/index.ts', + '!./src/**/*.test-d.ts', + ], // The directory where Jest should output its coverage files coverageDirectory: 'coverage', diff --git a/src/internal/eth/EthKeyring.ts b/src/internal/eth/EthKeyring.ts new file mode 100644 index 000000000..f921efc49 --- /dev/null +++ b/src/internal/eth/EthKeyring.ts @@ -0,0 +1,47 @@ +import type { Json, Keyring } from '@metamask/utils'; + +import type { + EthBaseTransaction, + EthBaseUserOperation, + EthUserOperation, + EthUserOperationPatch, +} from '../../eth'; + +export type EthKeyring = Keyring & { + /** + * Convert a base transaction to a base UserOperation. + * + * @param address - Address of the sender. + * @param transactions - Base transactions to include in the UserOperation. + * @returns A pseudo-UserOperation that can be used to construct a real. + */ + prepareUserOperation?( + address: string, + transactions: EthBaseTransaction[], + ): Promise; + + /** + * Patches properties of a UserOperation. Currently, only the + * `paymasterAndData` can be patched. + * + * @param address - Address of the sender. + * @param userOp - UserOperation to patch. + * @returns A patch to apply to the UserOperation. + */ + patchUserOperation?( + address: string, + userOp: EthUserOperation, + ): Promise; + + /** + * Signs an UserOperation. + * + * @param address - Address of the sender. + * @param userOp - UserOperation to sign. + * @returns The signature of the UserOperation. + */ + signUserOperation?( + address: string, + userOp: EthUserOperation, + ): Promise; +}; diff --git a/src/internal/eth/index.ts b/src/internal/eth/index.ts new file mode 100644 index 000000000..fa5f8282c --- /dev/null +++ b/src/internal/eth/index.ts @@ -0,0 +1 @@ +export * from './EthKeyring'; diff --git a/src/internal/index.ts b/src/internal/index.ts index 9b0ba884b..3b1199364 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -1,4 +1,5 @@ export * from './api'; +export * from './eth'; export * from './events'; export * from './rpc'; export * from './types'; From 29fcb5e825923c6d7393cfed44e746b346133f59 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:21:13 +0100 Subject: [PATCH 16/17] fix: add UserOperation methods to support methods enum --- src/api.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api.ts b/src/api.ts index 5a5b4e1c6..c96908411 100644 --- a/src/api.ts +++ b/src/api.ts @@ -58,6 +58,9 @@ export const KeyringAccountStruct = object({ `${EthMethod.SignTypedDataV1}`, `${EthMethod.SignTypedDataV3}`, `${EthMethod.SignTypedDataV4}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, ]), ), From 6741d22ab6ac5dab22708db667c966eaf3817f13 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:08:01 +0100 Subject: [PATCH 17/17] docs: add link to ERC-4337 spec --- docs/evm_methods_userOp.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/evm_methods_userOp.md b/docs/evm_methods_userOp.md index 3c8f69703..5f766d891 100644 --- a/docs/evm_methods_userOp.md +++ b/docs/evm_methods_userOp.md @@ -1,7 +1,7 @@ # EVM Methods for ERC-4337 Accounts Here we document the methods that an account Snap may implement to support -requests originated from dapps. +requests using [ERC-4337][erc-4337] accounts. ## eth_prepareUserOperation @@ -267,3 +267,5 @@ Sign an UserOperation. ```json "0x6565acc7efd3c85e4c0c221c2958ff6c3ae68401b23b33fdcd1a2d49034c30d97b1cfa17487b90253a5dfd54ef5188688592c2fd56ba44ee4d948ea259d636cd550f6dd21b" ``` + +[erc-4337]: https://eips.ethereum.org/EIPS/eip-4337